├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .luacheckrc ├── LICENSE.md ├── README.md ├── Screenshot.png ├── digistuff_ts.lua ├── grid.png ├── index.html ├── index.js ├── index.lua ├── json.lua ├── renderer.lua ├── start-server ├── style.css └── style.scss /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache/ 2 | style.css.map 3 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: roangzero1/factorio-mod:luarocks5.3-alpine 2 | 3 | luacheck: 4 | stage: test 5 | script: 6 | - luacheck . 7 | 8 | pages: 9 | stage: deploy 10 | script: 11 | - git submodule update --recursive --init 12 | - mkdir public 13 | - cp -r fs51 formspec_ast *.lua *.js *.html grid.png public 14 | - grep -vF '/*# sourceMappingURL=style.css.map */' < style.css > 15 | public/style.css 16 | - rm -rf public/*/.git public/*/.github public/*/.luacheckrc 17 | public/formspec_ast/*.py public/*/mod.conf public/*/.gitignore 18 | public/*/depends.txt 19 | - | 20 | set -e 21 | cd public 22 | grep -oE 'https://unpkg.com/[a-z\-]+/dist/[a-z\.\-]+.js' \ 23 | index.html | while read url; do 24 | wget "$url" 25 | wget "$url.map" || true 26 | done 27 | - sed -i 's|https://unpkg.com/[a-z\-]*/dist/\([a-z\.\-]*.js\)|\1|g' 28 | index.html 29 | artifacts: 30 | paths: 31 | - public 32 | only: 33 | - master 34 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "formspec_ast"] 2 | path = formspec_ast 3 | url = https://gitlab.com/luk3yx/minetest-formspec_ast.git 4 | [submodule "fs51"] 5 | path = fs51 6 | url = https://gitlab.com/luk3yx/minetest-fs51.git 7 | branch = main 8 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | unused_args = false 2 | allow_defined_top = true 3 | 4 | read_globals = { 5 | formspec_ast = { 6 | fields = {"apply_offset", "find", "flatten", "formspec_escape", 7 | "get_element_by_name", "get_elements_by_name", "interpret", 8 | "parse", "register_element", "safe_interpret", "safe_parse", 9 | "show_formspec", "unparse", "walk"} 10 | }, 11 | fs51 = { 12 | fields = {"backport", "backport_string"} 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web-based formspec edtior 2 | 3 | Really actually removing the pains of formspec design. 4 | 5 | [Try it online](https://luk3yx.gitlab.io/minetest-formspec-editor/) 6 | 7 | Uses [Fengari](https://fengari.io/) to run my 8 | [formspec_ast](https://content.minetest.net/packages/luk3yx/formspec_ast/) and 9 | [fs51](https://content.minetest.net/packages/luk3yx/fs51/) mods on 10 | web browsers. 11 | 12 | `image[]` elements use [HDX](https://github.com/mt-historical/hdx-128) textures 13 | by default (dynamically loaded when required). 14 | 15 | ## Major features 16 | 17 | - Web-based (no waiting for MT to load) 18 | - Dragging and resizing elements. 19 | - Property editor 20 | - `${lua code}` substitution in text values. 21 | - Don't remove the weird comments generated when exporting these formspecs 22 | if you plan to import them again. 23 | - The ability to load existing formspecs (provided they are version 2 or 24 | above). 25 | - The ability to export to (but not import from) digistuff touchscreen 26 | formspecs. 27 | 28 | ## Limitations 29 | 30 | - Although it can save formspecs in the version 1 format, it cannot load them 31 | in this format. Co-ordinates are backported with help from my `fs51` mod. 32 | - The properties editor is slow when manipulating lots of properties. 33 | - Malicious formspecs imported with the `${...}` substitution option enabled 34 | can freeze the webpage. 35 | - Element alignment might not be perfect. 36 | - I haven't tested this thoroughly in many browsers, if you find any bugs 37 | please report them. 38 | - Texture modifiers in `image[]` will not be displayed in the preview. 39 | 40 | ## Copyright / License 41 | 42 | Copyright © 2020 by luk3yx 43 | 44 | This program is free software: you can redistribute it and/or modify 45 | it under the terms of the GNU Affero General Public License as 46 | published by the Free Software Foundation, either version 3 of the 47 | License, or (at your option) any later version. 48 | 49 | This program is distributed in the hope that it will be useful, 50 | but WITHOUT ANY WARRANTY; without even the implied warranty of 51 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 52 | GNU Affero General Public License for more details. 53 | 54 | You should have received a copy of the GNU Affero General Public License 55 | along with this program. If not, see . 56 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luk3yx/minetest-formspec-editor/6957f7ea88d0406aa895a8ee6ded657d38d72a20/Screenshot.png -------------------------------------------------------------------------------- /digistuff_ts.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Formspec AST to digistuff touchscreen converter 3 | -- 4 | -- Copyright © 2020 by luk3yx. 5 | -- 6 | -- This program is free software: you can redistribute it and/or modify 7 | -- it under the terms of the GNU Affero General Public License as 8 | -- published by the Free Software Foundation, either version 3 of the 9 | -- License, or (at your option) any later version. 10 | -- 11 | -- This program is distributed in the hope that it will be useful, 12 | -- but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | -- GNU Affero General Public License for more details. 15 | -- 16 | -- You should have received a copy of the GNU Affero General Public License 17 | -- along with this program. If not, see . 18 | -- 19 | 20 | local basic_types = {} 21 | for _, t in ipairs({'image', 'field', 'pwdfield', 'textarea', 'label', 22 | 'vertlabel', 'button', 'button_exit'}) do 23 | basic_types[t] = true 24 | end 25 | 26 | local function export(tree, backport_func) 27 | tree = formspec_ast.flatten(tree) 28 | 29 | -- Modify box nodes before calling backport_func() to fix alignment 30 | for node in formspec_ast.find(tree, 'box') do 31 | -- Boxes can be partially emulated with images. 32 | node.type = 'image' 33 | node.texture_name = 'halo.png^[colorize:' .. node.color 34 | node.color = nil 35 | end 36 | 37 | if backport_func then 38 | tree = backport_func(tree) 39 | end 40 | 41 | local fs_v2 = (tree.formspec_version or 1) >= 2 42 | tree.formspec_version = nil 43 | if tree[1] and tree[1].type == 'size' then 44 | table.remove(tree, 1) 45 | end 46 | for _, node in ipairs(tree) do 47 | if node.type == 'dropdown' or node.type == 'textlist' then 48 | node.selected_id, node.selected_idx = node.selected_idx, nil 49 | node.choices, node.items = node.items, nil 50 | -- Later versions of the digustuff mod require a height field even 51 | -- for formspec version 1. 52 | node.h = node.h or 0.81 53 | elseif node.type == 'image_button' or 54 | node.type == 'image_button_exit' then 55 | node.image, node.texture_name = node.texture_name, nil 56 | elseif not basic_types[node.type] then 57 | return nil, 'Unsupported node type: ' .. node.type 58 | end 59 | 60 | if not node.command then 61 | node.command = 'add' .. node.type 62 | end 63 | node.type = nil 64 | for _, i in ipairs({"x", "y", "w", "h"}) do 65 | if node[i] then 66 | node[i:upper()] = node[i] 67 | node[i] = nil 68 | end 69 | end 70 | end 71 | table.insert(tree, 1, {command = 'clear'}) 72 | table.insert(tree, 2, {command = 'realcoordinates', enabled = fs_v2}) 73 | return tree 74 | end 75 | 76 | local function very_basic_dump(obj) 77 | local obj_type = type(obj) 78 | if obj_type == 'string' then 79 | return ('%q'):format(obj) 80 | elseif obj_type ~= 'table' then 81 | return tostring(obj) 82 | end 83 | 84 | local t = {} 85 | for k, v in pairs(obj) do 86 | if type(k) ~= 'string' or not k:match('^[A-Za-z_]+$') then 87 | k = '[' .. very_basic_dump(k) .. ']' 88 | end 89 | local line = k .. ' = ' .. very_basic_dump(v) 90 | if k == 'command' then 91 | table.insert(t, 1, line) 92 | else 93 | table.insert(t, line) 94 | end 95 | end 96 | 97 | return '{' .. table.concat(t, ', ') .. '}' 98 | end 99 | 100 | local function export_string(...) 101 | local res, err = export(...) 102 | if not res then 103 | return nil, err 104 | end 105 | for k, v in ipairs(res) do 106 | res[k] = very_basic_dump(v) 107 | end 108 | return '{' .. table.concat(res, ',\n') .. '}' 109 | end 110 | 111 | return export, export_string 112 | -------------------------------------------------------------------------------- /grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luk3yx/minetest-formspec-editor/6957f7ea88d0406aa895a8ee6ded657d38d72a20/grid.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (() => { 4 | 5 | window.addEventListener("beforeunload", e => { 6 | return (e || window.event).returnValue = "Are you sure you want to go?"; 7 | }); 8 | 9 | if (window.location.search == "?no-drag-drop") 10 | return; 11 | 12 | window.basic_interact = {}; 13 | 14 | let snap; 15 | basic_interact.snap = () => { 16 | if (!snap) 17 | snap = interact.modifiers.snap({ 18 | targets: [ 19 | interact.snappers.grid({x: 5, y: 5}), 20 | ], 21 | range: Infinity, 22 | offset: 'self', 23 | relativePoints: [{x: 0, y: 0}] 24 | }) 25 | return snap; 26 | }; 27 | 28 | basic_interact.add = (target, draggable, resizable, callback, 29 | smallResizeMargin) => { 30 | let x = 0; 31 | let y = 0; 32 | target.style.touchAction = "none"; 33 | if (target.style.userSelect) 34 | target.setAttribute("data-old-user-select", target.style.userSelect); 35 | target.style.userSelect = "none"; 36 | target.classList.add("drag_drop"); 37 | 38 | if (resizable) 39 | target.style.boxSizing = "border-box"; 40 | 41 | function move() { 42 | target.style.transform = `translate(${x}px, ${y}px)`; 43 | }; 44 | move(); 45 | 46 | function endMove() { 47 | callback.call(target, x, y, target.offsetWidth, target.offsetHeight); 48 | }; 49 | 50 | const interact_target = interact(target).on("tap", () => { 51 | callback.call(target); 52 | }); 53 | 54 | if (draggable) 55 | interact_target.draggable({ 56 | // inertia: true, 57 | listeners: { 58 | end: endMove, 59 | move (event) { 60 | x += event.dx; 61 | y += event.dy; 62 | move(); 63 | }, 64 | }, 65 | modifiers: [ 66 | interact.modifiers.restrictRect({ 67 | restriction: 'parent', 68 | endOnly: true, 69 | }), 70 | basic_interact.snap() 71 | ], 72 | }); 73 | 74 | if (resizable) 75 | interact_target.resizable({ 76 | edges: { 77 | top: true, 78 | left: true, 79 | bottom: true, 80 | right: true, 81 | }, 82 | listeners: { 83 | end: endMove, 84 | move (event) { 85 | target.style.width = `${event.rect.width}px`; 86 | target.style.height = `${event.rect.height}px`; 87 | x += event.deltaRect.left; 88 | y += event.deltaRect.top; 89 | move(); 90 | } 91 | }, 92 | modifiers: [ 93 | // interact.modifiers.restrictRect({ 94 | // restriction: 'parent', 95 | // endOnly: true, 96 | // }), 97 | basic_interact.snap() 98 | ], 99 | margin: smallResizeMargin ? 10 : 20, 100 | invert: "reposition", 101 | }); 102 | }; 103 | 104 | basic_interact.remove = target => { 105 | interact(target).unset(); 106 | target.style.transform = ""; 107 | target.style.touchAction = ""; 108 | const old_user_select = target.getAttribute("data-old-user-select"); 109 | target.style.userSelect = old_user_select || ""; 110 | target.removeAttribute("data-old-user-select"); 111 | target.classList.remove("drag_drop"); 112 | delete basic_interact.target; 113 | }; 114 | 115 | window.addEventListener("load", () => { 116 | const elem = document.createElement("style"); 117 | elem.innerHTML = ` 118 | .drag_drop * { 119 | cursor: inherit; 120 | } 121 | `; 122 | document.head.appendChild(elem); 123 | }); 124 | 125 | })(); 126 | -------------------------------------------------------------------------------- /index.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Web-based formspec editor 3 | -- 4 | -- Copyright © 2020 by luk3yx. 5 | -- 6 | -- This program is free software: you can redistribute it and/or modify 7 | -- it under the terms of the GNU Affero General Public License as 8 | -- published by the Free Software Foundation, either version 3 of the 9 | -- License, or (at your option) any later version. 10 | -- 11 | -- This program is distributed in the hope that it will be useful, 12 | -- but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | -- GNU Affero General Public License for more details. 15 | -- 16 | -- You should have received a copy of the GNU Affero General Public License 17 | -- along with this program. If not, see . 18 | -- 19 | 20 | -- Load the renderer 21 | dofile('renderer.lua?rev=11') 22 | local formspec_escape = formspec_ast.formspec_escape 23 | 24 | local _, digistuff_ts_export = dofile('digistuff_ts.lua?rev=4') 25 | 26 | -- Show the properties list 27 | local properties_elem 28 | local function get_properties_list(list_name) 29 | local res = {} 30 | local elems = properties_elem.firstChild.firstChild.children 31 | for i = 0, elems.length - 1 do 32 | local elem = elems[i] 33 | local name = elem:getAttribute('data-formspec_ast-name') 34 | if type(name) == 'string' and name:sub(1, 5) == 'list[' then 35 | local s, e = name:find(']', nil, true) 36 | local n = name:sub(e + 1) 37 | if n == list_name then 38 | res[tonumber(name:sub(6, s - 1))] = elem.lastChild.value 39 | end 40 | end 41 | end 42 | return res 43 | end 44 | 45 | local function get_properties_map(list_name) 46 | local keys = {} 47 | local values = {} 48 | local elems = properties_elem.firstChild.firstChild.children 49 | for i = 0, elems.length - 1 do 50 | local elem = elems[i] 51 | local name = elem:getAttribute('data-formspec_ast-name') 52 | if type(name) == 'string' then 53 | if name:sub(1, 5) == 'map1[' then 54 | local s, e = name:find(']', nil, true) 55 | local n = name:sub(e + 1) 56 | if n == list_name then 57 | keys[tonumber(name:sub(6, s - 1))] = elem.lastChild.value 58 | end 59 | elseif name:sub(1, 5) == 'map2[' then 60 | local s, e = name:find(']', nil, true) 61 | local n = name:sub(e + 1) 62 | if n == list_name then 63 | values[tonumber(name:sub(6, s - 1))] = elem.lastChild.value 64 | end 65 | end 66 | end 67 | end 68 | local res = {} 69 | for i, key in ipairs(keys) do 70 | res[key] = assert(values[i]) 71 | end 72 | return res 73 | end 74 | 75 | local property_names = { 76 | h = 'Height', 77 | w = 'Width', 78 | drawborder = 'Draw border', 79 | listelems = 'Items', 80 | selected_idx = 'Selected item', 81 | props = 'Properties', 82 | opt = 'Options', 83 | } 84 | local function get_property_name(n) 85 | return property_names[n] or n:sub(1, 1):upper() .. n:sub(2):gsub('_', ' ') 86 | end 87 | 88 | local function draw_elements_list(selected_element) 89 | local formspec = 'label[0.25,0.5;Selected element]' .. 90 | 'dropdown[0.25,1;5.5,0.75;selected_element;' 91 | local selected = 0 92 | local elems = selected_element.parentElement.children 93 | local rendered_elems = 0 94 | for i = 0, elems.length - 1 do 95 | local elem = elems[i] 96 | if elem:getAttribute('data-transient') == 'true' then 97 | goto continue 98 | end 99 | if rendered_elems > 0 then 100 | formspec = formspec .. ',' 101 | end 102 | rendered_elems = rendered_elems + 1 103 | formspec = formspec .. formspec_escape(elem:getAttribute('data-type')) 104 | if elem == selected_element then 105 | selected = rendered_elems 106 | end 107 | ::continue:: 108 | end 109 | if selected == 0 then 110 | selected = rendered_elems + 1 111 | if rendered_elems > 0 then 112 | formspec = formspec .. ',' 113 | end 114 | formspec = formspec .. '(New element)' 115 | end 116 | return formspec .. ';' .. selected .. ']' 117 | end 118 | 119 | local SCALE = 50 120 | local function round_pos(pos) 121 | return math.floor(pos * 10 + 0.5) / 10 122 | end 123 | 124 | local function show_properties(elem, node) 125 | if not properties_elem then 126 | properties_elem = document:createElement('div') 127 | properties_elem.id = 'formspec_ast-properties' 128 | document.body:appendChild(properties_elem) 129 | end 130 | properties_elem.innerHTML = '' 131 | if type(node) ~= 'table' then 132 | node = nil 133 | end 134 | node = node or json.loads(elem:getAttribute('data-formspec_ast')) 135 | 136 | -- Why not do this as a formspec? 137 | local callbacks = {} 138 | local formspec = draw_elements_list(elem) .. 139 | 'label[0.25,2.5;Properties for ' .. formspec_escape(node.type) .. ']' 140 | local y = 3.5 141 | for k_, v in pairs(node) do 142 | if k_ == 'type' or k_ == '_transient' then goto continue end 143 | 144 | local k = k_ 145 | local value_type = type(v) 146 | if k == 'opt' or k == 'props' then 147 | assert(value_type == 'table') 148 | formspec = formspec .. 'label[0.25,' .. y - 0.2 .. ';' .. 149 | formspec_escape(get_property_name(k)) .. ' (map)]' 150 | y = y + 0.1 151 | local i = 0 152 | for prop_, value in pairs(v) do 153 | local prop = prop_ 154 | i = i + 1 155 | formspec = formspec .. 'label[0.4,' .. y + 0.3 .. ';•]' .. 156 | 'field[0.7,' .. y .. ';1.95,0.6;' .. 157 | formspec_escape('map1[' .. i .. ']' .. k) .. ';;' .. 158 | formspec_escape(tostring(prop)) .. ']' .. 159 | 'label[2.7,' .. y + 0.3 .. ';=]' .. 160 | 'field[2.95,' .. y .. ';2,0.6;' .. 161 | formspec_escape('map2[' .. i .. ']' .. k) .. ';;' .. 162 | formspec_escape(tostring(value)) .. ']' .. 163 | 'button[5.15,' .. y .. ';0.6,0.6;' .. 164 | formspec_escape('map-' .. i .. ':' .. k) .. ';X]' 165 | y = y + 0.8 166 | 167 | callbacks['map-' .. i .. ':' .. k] = function() 168 | node[k] = get_properties_map(k) 169 | node[k][prop] = nil 170 | show_properties(elem, node) 171 | end 172 | end 173 | formspec = formspec .. 'button[0.25,' .. y .. ';5.5,0.6;' .. 174 | formspec_escape('props+' .. k) .. ';Add item]' 175 | callbacks['props+' .. k] = function() 176 | node[k] = get_properties_map(k) 177 | node[k][''] = '' 178 | show_properties(elem, node) 179 | end 180 | y = y + 1.3 181 | goto continue 182 | elseif value_type == 'table' then 183 | -- This table generation code is bad, the entire properties 184 | -- formspec is redrawn when a table element is deleted/created, 185 | -- however the "reset" button works. 186 | formspec = formspec .. 'label[0.25,' .. y - 0.2 .. ';' .. 187 | formspec_escape(get_property_name(k)) .. ' (list)]' 188 | y = y + 0.1 189 | for i, item in ipairs(v) do 190 | formspec = formspec .. 'label[0.4,' .. y + 0.3 .. ';•]' .. 191 | 'field[0.7,' .. y .. ';4.25,0.6;' .. 192 | formspec_escape('list[' .. i .. ']' .. k) .. ';;' .. 193 | formspec_escape(tostring(item)) .. ']' .. 194 | 'button[5.15,' .. y .. ';0.6,0.6;' .. 195 | formspec_escape('list-' .. i .. ':' .. k) .. ';X]' 196 | y = y + 0.8 197 | callbacks['list-' .. i .. ':' .. k] = function() 198 | node[k] = get_properties_list(k) 199 | table.remove(node[k], i) 200 | show_properties(elem, node) 201 | end 202 | end 203 | formspec = formspec .. 'button[0.25,' .. y .. ';5.5,0.6;' .. 204 | formspec_escape('list+' .. k) .. ';Add item]' 205 | callbacks['list+' .. k] = function() 206 | node[k] = get_properties_list(k) 207 | table.insert(node[k], '') 208 | show_properties(elem, node) 209 | end 210 | y = y + 1.3 211 | goto continue 212 | end 213 | 214 | if value_type == 'boolean' then 215 | formspec = formspec .. 'checkbox[0.25,' .. y 216 | y = y + 0.8 217 | else 218 | formspec = formspec .. 'field[0.25,' .. y .. ';5.5,0.6' 219 | y = y + 1.1 220 | end 221 | formspec = formspec .. ';' .. formspec_escape('prop_' .. k) .. ';' .. 222 | formspec_escape(get_property_name(k) .. ' (' .. value_type .. ')') 223 | .. ';' .. formspec_escape(tostring(v)) .. ']' 224 | ::continue:: 225 | end 226 | 227 | if node._transient then 228 | formspec = formspec .. 229 | 'button[0.25,' .. y .. ';2.7,0.75;delete;Cancel]' .. 230 | 'button[3.05,' .. y .. ';2.7,0.75;reset;Reset]' 231 | y = y + 0.85 232 | else 233 | formspec = formspec .. 234 | 'button[0.25,' .. y .. ';2.7,0.75;send_to_back;Send to back]' .. 235 | 'button[3.05,' .. y .. ';2.7,0.75;bring_to_front;Bring to front]' .. 236 | 'button[0.25,' .. y + 0.85 .. ';5.5,0.75;duplicate;Duplicate]' .. 237 | 'button[0.25,' .. y + 1.7 .. ';2.7,0.75;delete;Delete element]' .. 238 | 'button[3.05,' .. y + 1.7 .. ';2.7,0.75;reset;Reset]' 239 | y = y + 2.55 240 | end 241 | 242 | formspec = 'formspec_version[2]size[6,' .. y + 1.25 .. ']' .. formspec .. 243 | 'button[0.25,' .. y.. ';5.5,1;save;' 244 | if node.type == 'size' then 245 | formspec = formspec .. 'Resize formspec' 246 | elseif node._transient then 247 | formspec = formspec .. 'Create element' 248 | else 249 | formspec = formspec .. 'Save changes' 250 | end 251 | formspec = formspec .. ']' 252 | 253 | local counter = 0 254 | function callbacks.duplicate() 255 | local parent = elem.parentNode 256 | elem = elem:cloneNode(true) 257 | parent:appendChild(elem) 258 | counter = counter + 1 259 | -- Set up drag+drop 260 | renderer.default_elem_hook(node, elem, SCALE) 261 | properties_elem:querySelector( 262 | '[data-formspec_ast-name="duplicate"]' 263 | ).textContent = "Duplicated x" .. counter 264 | end 265 | 266 | function callbacks.delete() 267 | if js.global:confirm('Are you sure?') then 268 | elem.parentNode:removeChild(elem) 269 | properties_elem.innerHTML = '' 270 | end 271 | end 272 | 273 | function callbacks.reset() 274 | show_properties(elem) 275 | end 276 | 277 | function callbacks.save() 278 | local elems = properties_elem.firstChild.firstChild.children 279 | local keys = {} 280 | for i = 0, elems.length - 1 do 281 | local e = elems[i] 282 | local name = e:getAttribute('data-formspec_ast-name') 283 | local prefix = type(name) == 'string' and name:sub(1, 5) 284 | if prefix == 'prop_' then 285 | local k = name:sub(6) 286 | if type(node[k]) == 'string' then 287 | node[k] = e.lastChild.value 288 | elseif type(node[k]) == 'number' then 289 | -- Allow commas to be used as decimal points. 290 | local raw = e.lastChild.value:gsub(',', '.') 291 | node[k] = tonumber(raw) or node[k] 292 | elseif type(node[k]) == 'boolean' then 293 | node[k] = e:getAttribute('data-checked') == 'true' 294 | end 295 | elseif prefix == 'list[' then 296 | local s = name:find(']', nil, true) 297 | local k = name:sub(s + 1) 298 | node[k][tonumber(name:sub(6, s - 1))] = e.lastChild.value 299 | elseif prefix == 'map1[' then 300 | local s = name:find(']', nil, true) 301 | local k = name:sub(s + 1) 302 | if not keys[k] then 303 | keys[k] = {} 304 | node[k] = {} 305 | end 306 | keys[k][tonumber(name:sub(6, s - 1))] = e.lastChild.value 307 | elseif prefix == 'map2[' then 308 | local s = name:find(']', nil, true) 309 | local k = name:sub(s + 1) 310 | local key = keys[k][tonumber(name:sub(6, s - 1))] 311 | node[k][key] = e.lastChild.value 312 | end 313 | end 314 | 315 | if node.type == 'image_button' and node.texture_name == '' then 316 | node.texture_name = 'blank.png' 317 | end 318 | 319 | node._transient = nil 320 | elem:setAttribute('data-formspec_ast', json.dumps(node)) 321 | properties_elem.innerHTML = '' 322 | local base = elem.parentNode.parentNode 323 | assert(base.classList:contains('formspec_ast-base')) 324 | local idx = window.Array.prototype.indexOf(elem.parentNode.children, 325 | elem) 326 | base = renderer.redraw_formspec(base) 327 | if node.type == 'size' then 328 | renderer.add_element(base, 'size') 329 | elseif idx >= 0 then 330 | show_properties(base.firstChild.children[idx]) 331 | end 332 | end 333 | 334 | function callbacks.send_to_back() 335 | local parent = elem.parentNode 336 | parent:removeChild(elem) 337 | parent:prepend(elem) 338 | show_properties(elem, node) 339 | end 340 | 341 | function callbacks.bring_to_front() 342 | local parent = elem.parentNode 343 | parent:removeChild(elem) 344 | parent:appendChild(elem) 345 | show_properties(elem, node) 346 | end 347 | 348 | local n = assert(renderer.render_formspec(formspec, callbacks, 349 | {store_json = false})) 350 | 351 | local elems = n.firstChild.children 352 | for i = 0, elems.length - 1 do 353 | local elem2 = elems[i] 354 | local name = elem2:getAttribute('data-formspec_ast-name') 355 | if name == 'selected_element' then 356 | elem2:addEventListener('change', function() 357 | local idx = elem2.firstChild.selectedIndex 358 | show_properties(elem.parentElement.children[idx]) 359 | end) 360 | break 361 | end 362 | end 363 | 364 | properties_elem:appendChild(n) 365 | end 366 | 367 | -- Set up drag+drop. This is mostly done in JavaScript for performance. 368 | function renderer.default_elem_hook(node, elem, scale) 369 | local basic_interact = js.global.basic_interact 370 | if not basic_interact then return show_properties end 371 | 372 | local draggable = node.x ~= nil and node.y ~= nil 373 | local resizable = node.w ~= nil and node.h ~= nil and node.type ~= "list" 374 | 375 | local small_resize_margin = false 376 | if resizable and (node.w * scale < 60 or node.h * scale < 60) then 377 | small_resize_margin = true 378 | end 379 | 380 | local orig_x, orig_y = node.x, node.y 381 | basic_interact:add(elem, draggable, resizable, function(_, x, y, w, h) 382 | local modified 383 | if draggable and x then 384 | node.x = round_pos(orig_x + x / SCALE) 385 | node.y = round_pos(orig_y + y / SCALE) 386 | modified = true 387 | end 388 | if resizable and w then 389 | node.w = round_pos(math.max(w / SCALE, 0.1)) 390 | node.h = round_pos(math.max(h / SCALE, 0.1)) 391 | modified = true 392 | end 393 | 394 | if modified then 395 | elem:setAttribute('data-formspec_ast', json.dumps(node)) 396 | local idx = window.Array.prototype.indexOf( 397 | elem.parentNode.children, elem) 398 | local base = renderer.redraw_formspec(elem.parentNode.parentNode) 399 | if idx >= 0 then 400 | show_properties(base.firstChild.children[idx]) 401 | end 402 | else 403 | show_properties(elem) 404 | end 405 | end, small_resize_margin) 406 | 407 | return true 408 | end 409 | 410 | -- Templates for new elements 411 | do 412 | local templates = assert(formspec_ast.parse([[ 413 | size[10.5,11] 414 | box[0,0;1,1;] 415 | button[0,0;3,0.8;;] 416 | button_exit[0,0;3,0.8;;] 417 | checkbox[0,0.2;;;false] 418 | dropdown[0,0;3,0.8;;;1;false] 419 | field[0,0;3,0.8;;;] 420 | image[0,0;1,1;] 421 | image_button[0,0;2,2;;;;false;true;] 422 | image_button_exit[0,0;2,2;;;] 423 | label[0,0.2;] 424 | list[current_player;main;0,0;8,4;0] 425 | pwdfield[0,0;3,0.8;;] 426 | textarea[0,0;3,2;;;] 427 | textlist[0,0;5,3;;;1;false] 428 | ]])) 429 | renderer.templates = {} 430 | for _, node in ipairs(templates) do 431 | renderer.templates[node.type] = node 432 | end 433 | end 434 | 435 | function renderer.add_element(base, node_type) 436 | local elem = base.firstChild.lastChild 437 | if elem == js.null or elem:getAttribute('data-transient') ~= 'true' then 438 | elem = renderer.make('div') 439 | elem.style.display = 'none' 440 | base.firstChild:appendChild(elem) 441 | end 442 | local template 443 | if node_type == 'size' then 444 | template = { 445 | type = 'size', 446 | w = tonumber(base:getAttribute('data-w')) or 0, 447 | h = tonumber(base:getAttribute('data-h')) or 0, 448 | } 449 | else 450 | template = assert(renderer.templates[node_type], 'Unknown node!') 451 | end 452 | template._transient = true 453 | elem:setAttribute('data-formspec_ast', json.dumps(template)) 454 | elem:setAttribute('data-transient', 'true') 455 | show_properties(elem) 456 | end 457 | 458 | local element_dialog_base 459 | do 460 | local replace_formspec = renderer.replace_formspec 461 | function renderer.replace_formspec(elem, ...) 462 | local new_elem, err = replace_formspec(elem, ...) 463 | if new_elem and element_dialog_base == elem then 464 | element_dialog_base = new_elem 465 | end 466 | return new_elem, err 467 | end 468 | end 469 | 470 | local function render_into(base, formspec, callbacks) 471 | base.innerHTML = '' 472 | base:appendChild(assert(renderer.render_formspec(formspec, callbacks, 473 | {store_json = false}))) 474 | end 475 | 476 | local element_dialog 477 | local load_save_opts = {multiline = true} 478 | local function show_load_save_dialog() 479 | local callbacks = {} 480 | local formspec = [[ 481 | formspec_version[4]size[6,12]button[0,0;1,0.6;back;←] 482 | label[1.25,0.3;Load / save formspec] 483 | checkbox[0.25,1.3;use_v1;Use formspec version 1;]] .. 484 | (load_save_opts.use_v1 and 'true' or 'false') .. [[] 485 | label[0.75,1.9;Use this if you need compatibility] 486 | label[0.75,2.3;with Minetest 5.0.1 or earlier.] 487 | label[0.75,3;This only works when saving.] 488 | checkbox[0.25,4;format;Convert ${...} to lua expressions;]] .. 489 | (load_save_opts.format and 'true' or 'false') .. [[] 490 | label[0.75,4.6;When this is enabled\, lua] 491 | label[0.75,5;expressions can be used inside] 492 | label[0.75,5.4;${...}. Formspec escaping is] 493 | label[0.75,5.8;handled automatically.] 494 | checkbox[0.25,6.8;multiline;One element per line;]] .. 495 | (load_save_opts.multiline and 'true' or 'false') .. [[] 496 | button[0.25,7.75;5.5,1;load;Load formspec] 497 | button[0.25,9;5.5,1;save;Save formspec] 498 | box[0,10.369;6,0.02;#aaa] 499 | button[0.25,10.75;5.5,1;digistuff_ts;Export to digistuff touchscreen] 500 | ]] 501 | local function get_options() 502 | local elems = element_dialog.firstChild.firstChild.children 503 | for i = 0, #elems - 1 do 504 | local elem = elems[i] 505 | local name = elem:getAttribute('data-formspec_ast-name') 506 | local checked = elem:getAttribute('data-checked') 507 | if type(name) == 'string' and type(checked) == 'string' then 508 | load_save_opts[name] = checked == 'true' 509 | end 510 | end 511 | end 512 | 513 | function callbacks.back() 514 | get_options() 515 | renderer.show_element_dialog(element_dialog_base) 516 | end 517 | 518 | local function load() 519 | local textarea = element_dialog.firstChild.firstChild.lastChild 520 | local fs = textarea.lastChild.value 521 | local tree, err = renderer.import(fs, load_save_opts) 522 | if not tree then 523 | window:alert('Error loading formspec:\n' .. err) 524 | return 525 | end 526 | local elem 527 | elem, err = renderer.replace_formspec(element_dialog_base, tree) 528 | if not elem then 529 | window:alert('Error loading formspec:\n' .. err) 530 | return 531 | end 532 | renderer.show_element_dialog(element_dialog_base) 533 | if properties_elem then 534 | properties_elem.innerHTML = '' 535 | end 536 | end 537 | 538 | function callbacks.load() 539 | get_options() 540 | local fs = 'formspec_version[2]size[6,9.5]button[0,0;1,0.6;back;←]' .. 541 | 'label[1.25,0.3;Load formspec]' .. 542 | 'button[0.25,8.25;5.5,1;load;Load formspec]' .. 543 | 'textarea[0.25,1.25;5.5,6.75;formspec;Paste your formspec here.;]' 544 | render_into(element_dialog, fs, { 545 | back = show_load_save_dialog, 546 | load = load 547 | }) 548 | end 549 | 550 | local function save_dialog(name, res, err) 551 | element_dialog.innerHTML = '' 552 | local label, msg 553 | if res then 554 | label, msg = 'Formspec exported successfully.', res 555 | else 556 | label, msg = 'Error exporting formspec!', err 557 | end 558 | local fs = 'formspec_version[2]size[6,9.5]button[0,0;1,0.6;back;←]' .. 559 | 'label[1.25,0.3;' .. name .. ']textarea[0.25,1.25;5.5,8;result;' .. 560 | label .. ';' .. formspec_escape(msg) .. ']' 561 | render_into(element_dialog, fs, { 562 | back = show_load_save_dialog, 563 | }) 564 | end 565 | 566 | function callbacks.save() 567 | get_options() 568 | local tree = renderer.elem_to_ast(element_dialog_base) 569 | save_dialog('Save formspec', renderer.export(tree, load_save_opts)) 570 | end 571 | 572 | function callbacks.digistuff_ts() 573 | get_options() 574 | local tree = renderer.elem_to_ast(element_dialog_base) 575 | local f = load_save_opts.use_v1 and renderer.fs51_backport or nil 576 | save_dialog('Export formspec', digistuff_ts_export(tree, f)) 577 | end 578 | 579 | render_into(element_dialog, formspec, callbacks) 580 | end 581 | 582 | function renderer.show_element_dialog(base) 583 | element_dialog_base = base 584 | if not element_dialog then 585 | element_dialog = document:createElement('div') 586 | element_dialog.id = 'formspec_ast-new' 587 | document.body:appendChild(element_dialog) 588 | end 589 | element_dialog.innerHTML = '' 590 | 591 | local fs = 'label[0.25,0.5;Add elements]' 592 | local callbacks = {} 593 | local y = 1.25 594 | 595 | for name, def in pairs(renderer.templates) do 596 | fs = fs .. 'button[0.25,' .. y .. ';5.5,0.75;' .. 597 | formspec_escape('add_' .. name) .. ';' .. 598 | formspec_escape(formspec_ast.unparse({def})) .. ']' 599 | y = y + 1 600 | local node_type = name 601 | callbacks['add_' .. name] = function() 602 | renderer.add_element(element_dialog_base, node_type) 603 | end 604 | end 605 | y = y + 0.5 606 | fs = fs .. 'button[0.25,' .. y .. ';5.5,0.75;grid;Toggle grid]' 607 | y = y + 1 608 | fs = fs .. 'button[0.25,' .. y .. ';5.5,0.75;load;Load / save formspec]' 609 | function callbacks.grid() 610 | local raw = element_dialog_base:getAttribute('data-render-options') 611 | if raw == js.null then raw = '{}' end 612 | local options = json.loads(raw) 613 | options.grid = not options.grid 614 | raw = assert(json.dumps(options)) 615 | element_dialog_base:setAttribute('data-render-options', raw) 616 | renderer.redraw_formspec(element_dialog_base) 617 | if properties_elem then 618 | properties_elem.innerHTML = '' 619 | end 620 | end 621 | callbacks.load = show_load_save_dialog 622 | 623 | if js.global.basic_interact then 624 | y = y + 1 625 | fs = fs .. 'button[0.25,' .. y .. 626 | ';5.5,0.75;drag_drop;Disable drag+drop]' 627 | function callbacks.drag_drop() 628 | window.location.search = '?no-drag-drop' 629 | end 630 | end 631 | 632 | fs = 'formspec_version[3]size[6,' .. y + 1 .. ']' .. fs 633 | element_dialog:appendChild(assert(renderer.render_formspec(fs, callbacks, 634 | {store_json = false}))) 635 | end 636 | 637 | -- A JS API for testing 638 | function window:render_formspec(fs, callbacks, options) 639 | local tree = assert(formspec_ast.parse(fs)) 640 | local elem = assert(renderer.render_ast(tree, callbacks, options)) 641 | local e = document:getElementById('formspec_output') 642 | if not e or e == js.null then 643 | window:addEventListener('load', function() 644 | window:render_formspec(fs, callbacks) 645 | end) 646 | return 647 | end 648 | e.innerHTML = '' 649 | e:appendChild(elem) 650 | renderer.show_element_dialog(elem) 651 | end 652 | 653 | function window:copy_formspec() 654 | local e = assert(document:getElementById('formspec_output')).firstChild 655 | window:alert(formspec_ast.unparse(renderer.elem_to_ast(e))) 656 | end 657 | 658 | function window:unrender_formspec(elem) 659 | return renderer.unrender_formspec(elem) 660 | end 661 | 662 | function window:redraw_formspec(elem) 663 | return renderer.redraw_formspec(elem) 664 | end 665 | 666 | function window:add_element(node_type) 667 | local e = assert(document:getElementById('formspec_output')).firstChild 668 | renderer.add_element(e, node_type) 669 | end 670 | 671 | function window:make_image(...) 672 | return renderer.make_image(...) 673 | end 674 | 675 | window:render_formspec('formspec_version[2]size[10.5,11]') 676 | -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | local js = require 'js' 2 | local JSON = js.global.JSON 3 | local null = js.null 4 | json = {} 5 | 6 | -- Recursively convert JavaScript objects to tables. 7 | local function object_to_table(obj, nullvalue) 8 | if obj == null then 9 | return nullvalue 10 | elseif type(obj) == 'number' then 11 | -- Cast to integer if possible 12 | local i = math.floor(obj) 13 | if i == obj then return i end 14 | return obj 15 | elseif type(obj) ~= 'userdata' then 16 | return obj 17 | end 18 | 19 | local res = {} 20 | local array = js.global.Array:isArray(obj) 21 | if array then obj:unshift(null) end 22 | for k, v in (array and ipairs or pairs)(obj) do 23 | res[k] = object_to_table(v, nullvalue) 24 | end 25 | return res 26 | end 27 | 28 | local function table_to_object(table) 29 | local array = true 30 | for k, v in pairs(table) do 31 | if type(k) ~= 'number' then 32 | array = false 33 | break 34 | end 35 | end 36 | 37 | if array then 38 | local res = js.global:Array() 39 | for _, elem in ipairs(table) do 40 | if type(elem) == 'table' then 41 | elem = table_to_object(elem) 42 | end 43 | res:push(elem) 44 | end 45 | return res 46 | else 47 | local res = js.global:Object() 48 | for k, v in pairs(table) do 49 | if type(v) == 'table' then 50 | v = table_to_object(v) 51 | end 52 | res[k] = v 53 | end 54 | return res 55 | end 56 | end 57 | 58 | -- Alias for JSON:parse so pcall can call it. 59 | local function raw_parse(json, nullvalue) 60 | local obj = JSON:parse(json) 61 | return object_to_table(obj, nullvalue) 62 | end 63 | 64 | function json.loads(json, nullvalue) 65 | local success, result = pcall(raw_parse, json, nullvalue) 66 | if success then 67 | return result, nil 68 | else 69 | return nil, result 70 | end 71 | end 72 | 73 | json.loads = raw_parse 74 | 75 | -- Another alias 76 | local function raw_write(data, styled) 77 | if styled then styled = 4 else styled = nil end 78 | return JSON:stringify(data, nil, styled) 79 | end 80 | 81 | -- The un-parsing is was going to be done entirely in JavaScript, but lua. 82 | function json.dumps(data, styled) 83 | if type(data) == 'table' then 84 | data = table_to_object(data) 85 | end 86 | 87 | local success, result = pcall(raw_write, data, styled) 88 | if success then 89 | return result, nil 90 | else 91 | return nil, result 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /renderer.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Web-based formspec editor 3 | -- 4 | -- Copyright © 2020 by luk3yx. 5 | -- 6 | -- This program is free software: you can redistribute it and/or modify 7 | -- it under the terms of the GNU Affero General Public License as 8 | -- published by the Free Software Foundation, either version 3 of the 9 | -- License, or (at your option) any later version. 10 | -- 11 | -- This program is distributed in the hope that it will be useful, 12 | -- but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | -- GNU Affero General Public License for more details. 15 | -- 16 | -- You should have received a copy of the GNU Affero General Public License 17 | -- along with this program. If not, see . 18 | -- 19 | 20 | -- Load formspec_ast 21 | FORMSPEC_AST_PATH = 'formspec_ast' 22 | dofile(FORMSPEC_AST_PATH .. '/init.lua') 23 | local formspec_escape = formspec_ast.formspec_escape 24 | 25 | -- Load fs51 to allow formspec_version[1] exports 26 | FS51_PATH = 'fs51' 27 | dofile(FS51_PATH .. '/init.lua?rev=1') 28 | 29 | -- Load the JSON interoperability code 30 | dofile('json.lua') 31 | 32 | js = require 'js' 33 | window = js.global 34 | document = window.document 35 | 36 | renderer = {} 37 | 38 | local type = type 39 | 40 | -- Render formspecs to HTML 41 | local elems = {} 42 | 43 | local function update(src, dest) 44 | for k, v in pairs(src) do 45 | if type(v) == 'table' and dest[k] then 46 | update(dest[k], v) 47 | else 48 | dest[k] = v 49 | end 50 | end 51 | end 52 | 53 | local function make(elem_type, props, attrs) 54 | local elem = document:createElement(elem_type) 55 | if props then 56 | update(props, elem) 57 | end 58 | if attrs then 59 | for k, v in pairs(attrs) do 60 | elem:setAttribute(k:gsub('_', '-'), v) 61 | end 62 | end 63 | return elem 64 | end 65 | renderer.make = make 66 | 67 | function elems.label(node) 68 | return make('span', { 69 | textContent = node.label, 70 | }, { 71 | data_text = node.label, 72 | }) 73 | end 74 | 75 | function elems.vertlabel(node) 76 | return make('span', { 77 | textContent = node.label:gsub('', '\n'):sub(2, -2), 78 | }, { 79 | data_text = node.label, 80 | }) 81 | end 82 | 83 | function elems.button(node) 84 | return make('div', { 85 | textContent = node.label, 86 | }) 87 | end 88 | elems.button_exit = elems.button 89 | 90 | function elems.image_button(node) 91 | local res = make('div', nil, { 92 | data_drawborder = tostring(node.drawborder ~= false), 93 | }) 94 | if node.texture_name ~= 'blank.png' then 95 | res:appendChild(renderer.make_image(node.texture_name, true)) 96 | end 97 | res:appendChild(make('span', {textContent = node.label})) 98 | return res 99 | end 100 | elems.image_button_exit = elems.image_button 101 | 102 | local function make_field(input_type, node, base, default_callbacks) 103 | local res = make('div') 104 | res:appendChild(make('span', {textContent = node.label})) 105 | local input = make('input', nil, { 106 | type = input_type, 107 | value = node.default or '', 108 | }) 109 | if default_callbacks then 110 | input:setAttribute('readonly', 'readonly') 111 | end 112 | res:appendChild(input) 113 | return res 114 | end 115 | 116 | function elems.field(...) 117 | return make_field('text', ...) 118 | end 119 | 120 | function elems.pwdfield(...) 121 | return make_field('password', ...) 122 | end 123 | 124 | function elems.textarea(node, base, default_callbacks) 125 | local res = make('div') 126 | res:appendChild(make('span', {textContent = node.label})) 127 | local textarea = make('textarea', nil, { 128 | type = 'text', 129 | }) 130 | textarea.textContent = node.default or '' 131 | if default_callbacks then 132 | textarea:setAttribute('readonly', 'readonly') 133 | end 134 | res:appendChild(textarea) 135 | return res 136 | end 137 | 138 | function elems.size(node, base, default_callbacks, scale) 139 | base.style.width = (node.w * scale) .. 'px' 140 | base.style.height = (node.h * scale) .. 'px' 141 | 142 | base:setAttribute('data-w', tostring(node.w)) 143 | base:setAttribute('data-h', tostring(node.h)) 144 | end 145 | 146 | function elems.image(node) 147 | return renderer.make_image(node.texture_name) 148 | end 149 | 150 | function elems.checkbox(node, base, default_callbacks) 151 | local checked = node.selected 152 | local div = make('div', nil, {data_checked = tostring(checked)}) 153 | div:appendChild(make('div')) 154 | div:appendChild(make('span', {textContent = node.label})) 155 | if not default_callbacks then 156 | div:addEventListener('click', function() 157 | checked = not checked 158 | div:setAttribute('data-checked', tostring(checked)) 159 | end) 160 | end 161 | return div 162 | end 163 | 164 | function elems.list(node, base, default_callbacks) 165 | local w, h = math.floor(node.w), math.floor(node.h) 166 | local res = make('table') 167 | for y = 1, h do 168 | local tr = make('tr') 169 | for x = 1, w do 170 | tr:appendChild(make('td')) 171 | end 172 | res:appendChild(tr) 173 | end 174 | res.style.left = node.x .. 'em' 175 | res.style.top = node.y .. 'em' 176 | res.style.width = (node.w * 1.25) .. 'em' 177 | res.style.height = (node.h * 1.25) .. 'em' 178 | return res, true 179 | end 180 | 181 | function elems.box(node) 182 | local res = make('div') 183 | res.style.backgroundColor = node.color 184 | if node.color:find('^ *rgb[^a]') or 185 | node.color:find('^ *#..[^ ] *$') or 186 | node.color:find('^ *#.....[^ ] *$') then 187 | res.style.opacity = '0.55' 188 | end 189 | return res 190 | end 191 | 192 | function elems.textlist(node) 193 | local res = make('div') 194 | for i, item in ipairs(node.listelems) do 195 | local elem = make('div') 196 | if item:sub(1, 1) ~= '#' then 197 | elem.textContent = item 198 | elseif item:sub(2, 2) == '#' then 199 | elem.textContent = item:sub(3) 200 | else 201 | elem.style.color = item:sub(1, 7) 202 | elem.textContent = item:sub(8) 203 | end 204 | if elem.textContent == '' then 205 | elem.innerHTML = ' ' 206 | end 207 | if i == node.selected_idx then 208 | elem.style.background = '#467832'; 209 | end 210 | res:appendChild(elem) 211 | end 212 | if node.transparent then 213 | res.style.background = 'none' 214 | res.style.borderColor = 'transparent' 215 | end 216 | return res 217 | end 218 | 219 | function elems.dropdown(node, base, default_callbacks, scale) 220 | local res = make('div') 221 | if not node.h then 222 | res.style.width = (node.w * scale) .. 'px' 223 | res.style.height = (2 * 15/13 * 0.35 * scale) .. 'px' 224 | end 225 | local select = make('select') 226 | for i, item in ipairs(node.items) do 227 | local e = make('option', {textContent = item}, {name = i}) 228 | if i == node.selected_idx then 229 | e:setAttribute('selected', 'selected') 230 | end 231 | select:appendChild(e) 232 | end 233 | window:setTimeout(function() 234 | if res.classList:contains('formspec_ast-clickable') then 235 | select:setAttribute('disabled', 'disabled') 236 | end 237 | end, 0) 238 | res:appendChild(select) 239 | 240 | local btn = make('div') 241 | btn:appendChild(make('div')) 242 | res:appendChild(btn) 243 | return res 244 | end 245 | 246 | local invisible_nodes = {style = true, position = true, anchor = true} 247 | local warned = {} 248 | local function generic_render(node) 249 | local visible = not invisible_nodes[node.type] 250 | if visible then 251 | if not warned[node.type] then 252 | warned[node.type] = true 253 | window.console:warn('Formspec element type ' .. node.type .. 254 | ' not implemented.') 255 | end 256 | if node.x and node.y then 257 | return renderer.make_image('unknown_object.png') 258 | end 259 | window.console:error('Formspec element type ' .. node.type .. 260 | ' is not implemented and there is no reliable way to' .. 261 | ' render it.') 262 | end 263 | 264 | local res = make('div') 265 | res.style.display = 'none' 266 | return res 267 | end 268 | 269 | -- Make images - This uses HDX to simplify things 270 | local image_baseurl = 'https://raw.githubusercontent.com/mt-historical/' .. 271 | 'hdx-128/master/' 272 | local mode_cache = {} 273 | function renderer.make_image(name, allow_empty) 274 | -- Remove extension 275 | local real_name = name:match('^(.*)%.[^%.]+$') or '' 276 | 277 | -- Make an element 278 | local img = document:createElement('img') 279 | local mode = mode_cache[name] or 'png' 280 | img:setAttribute('ondragstart', 'return false') 281 | if name == '' and allow_empty then 282 | img.style.opacity = '0' 283 | return img 284 | elseif name == '' or mode == '' then 285 | img.src = image_baseurl .. 'unknown_node.png' 286 | return img 287 | end 288 | 289 | img:addEventListener('error', function() 290 | if mode == 'png' then 291 | mode = 'jpg' 292 | mode_cache[name] = 'jpg' 293 | elseif mode == nil then 294 | return 295 | else 296 | mode = nil 297 | mode_cache[name] = '' 298 | img.src = image_baseurl .. 'unknown_node.png' 299 | return 300 | end 301 | img.src = image_baseurl .. real_name .. '.' .. mode 302 | end) 303 | img.src = image_baseurl .. real_name .. '.' .. mode 304 | return img 305 | end 306 | 307 | local default_options = {} 308 | function renderer.render_ast(tree, callbacks, options) 309 | options = options or default_options 310 | local scale = 50 * (options.scale or 1) 311 | local store_json = options.store_json or options.store_json == nil 312 | local base = document:createElement('div') 313 | base.className = 'formspec_ast-base' 314 | base:setAttribute('data-render-options', json.dumps(options)) 315 | base.style.fontSize = scale .. 'px' 316 | local container = document:createElement('div') 317 | base:appendChild(container) 318 | if options.grid then 319 | base.firstChild.className = 'grid' 320 | end 321 | 322 | for _, node in ipairs(formspec_ast.flatten(tree)) do 323 | if node.type == 'real_coordinates' then 324 | return nil, 'Unsupported element: real_coordinates[]' 325 | end 326 | 327 | -- Attempt to use a generic renderer 328 | local render_func = elems[node.type] or generic_render 329 | 330 | local e, ignore_pos = render_func(node, base, callbacks == nil, scale) 331 | if e then 332 | if node.x and node.y and not ignore_pos then 333 | e.style.left = (node.x * scale) .. 'px' 334 | e.style.top = (node.y * scale) .. 'px' 335 | if node.w and node.h then 336 | e.style.width = (node.w * scale) .. 'px' 337 | e.style.height = (node.h * scale) .. 'px' 338 | end 339 | end 340 | e.className = 'formspec_ast-element formspec_ast-' .. node.type 341 | if store_json or store_json == nil then 342 | e:setAttribute('data-formspec_ast', json.dumps(node)) 343 | e:setAttribute('data-type', node.type) 344 | end 345 | if node.name then 346 | e:setAttribute('data-formspec_ast-name', node.name) 347 | end 348 | local func 349 | if type(callbacks) == 'table' then 350 | func = callbacks[node.name or ''] 351 | elseif callbacks == nil then 352 | func = renderer.default_elem_hook(node, e, scale) 353 | end 354 | if func then 355 | if type(func) == 'function' then 356 | e:addEventListener('click', func) 357 | end 358 | e.classList:add('formspec_ast-clickable') 359 | end 360 | container:appendChild(e) 361 | end 362 | end 363 | container.style.width = base.style.width 364 | container.style.height = base.style.height 365 | return base 366 | end 367 | 368 | function renderer.render_formspec(formspec, ...) 369 | local tree, err = formspec_ast.parse(formspec) 370 | if err then 371 | return nil, err 372 | end 373 | return renderer.render_ast(tree, ...) 374 | end 375 | 376 | function renderer.elem_to_ast(elem) 377 | assert(elem.children.length == 1) 378 | local html_elems = elem.firstChild.children 379 | 380 | local w = tonumber(elem:getAttribute('data-w')) 381 | local h = tonumber(elem:getAttribute('data-h')) 382 | local res = { 383 | formspec_version = 6, 384 | { 385 | type = 'size', 386 | w = w or 0, 387 | h = h or 0, 388 | } 389 | } 390 | for i = 0, html_elems.length - 1 do 391 | local data = html_elems[i]:getAttribute('data-formspec_ast') 392 | local node = assert(json.loads(data), 'Error loading data!') 393 | 394 | if not node._transient then 395 | if node.name == 'size' then 396 | -- A hack to replace the existing size[] with any new one 397 | res[2] = node 398 | else 399 | res[#res + 1] = node 400 | end 401 | end 402 | end 403 | return res 404 | end 405 | 406 | function renderer.replace_formspec(elem, ...) 407 | local new_elem, err = renderer.render_ast(...) 408 | if not new_elem then return nil, err end 409 | elem:replaceWith(new_elem) 410 | return new_elem, nil 411 | end 412 | 413 | function renderer.redraw_formspec(elem) 414 | local tree = renderer.elem_to_ast(elem) 415 | local options = elem:getAttribute('data-render-options') 416 | if type(options) == 'string' then 417 | options = json.loads(options) 418 | else 419 | options = nil 420 | end 421 | return renderer.replace_formspec(elem, tree, nil, options) 422 | end 423 | 424 | function renderer.unrender_formspec(elem) 425 | local res = renderer.elem_to_ast(elem) 426 | return formspec_ast.unparse(res) 427 | end 428 | 429 | local load = rawget(_G, 'loadstring') or load 430 | local function deserialize(code) 431 | if code:byte(1) == 0x1b then return nil, 'Cannot load bytecode' end 432 | code = 'return ' .. code 433 | local f 434 | if rawget(_G, 'loadstring') and rawget(_G, 'setfenv') then 435 | f = loadstring(code) 436 | setfenv(f, {}) 437 | else 438 | f = load(code, nil, nil, {}) 439 | end 440 | local ok, res = pcall(f) 441 | if ok then 442 | return res, nil 443 | else 444 | return nil, res 445 | end 446 | end 447 | 448 | function renderer.import(fs, opts) 449 | if opts.format then 450 | fs = fs:gsub('" %.%. minetest.formspec_escape%(tostring%(' .. 451 | '%-%-%[%[${%]%]([^}]*)%-%-%[%[}%]%]%)%) %.%. "', function(s) 452 | return '${' .. ('%q'):format(formspec_escape(s)):sub(2, -2) .. '}' 453 | end) 454 | local err 455 | fs, err = deserialize(fs) 456 | if type(fs) ~= 'string' then 457 | return nil, err or 'That was valid Lua but not a valid formspec!' 458 | end 459 | elseif fs:sub(1, 1) == '"' then 460 | return nil, 'Did you mean to enable ${...} conversion?' 461 | end 462 | local tree, err = formspec_ast.parse(fs) 463 | if tree and tree.formspec_version < 2 then 464 | return nil, 'Only formspec versions >= 2 can be loaded!' 465 | end 466 | return tree, err 467 | end 468 | 469 | function renderer.fs51_backport(tree) 470 | tree = fs51.backport(tree) 471 | 472 | -- Round numbers to 2 decimal places 473 | local c = {'x', 'y', 'w', 'h'} 474 | for node in formspec_ast.walk(tree) do 475 | for _, k in ipairs(c) do 476 | if type(node[k]) == 'number' then 477 | node[k] = math.floor((node[k] * 100) + 0.5) / 100 478 | end 479 | end 480 | end 481 | return tree 482 | end 483 | 484 | function renderer.export(tree, opts) 485 | if opts.use_v1 then 486 | tree = renderer.fs51_backport(tree) 487 | end 488 | 489 | local fs, err = formspec_ast.unparse(tree) 490 | if not fs then return nil, err end 491 | 492 | if opts.multiline then 493 | -- Make sure escapes are properly handled 494 | fs = fs:gsub('\\*%]', function(data) 495 | if #data % 2 == 1 then 496 | data = data .. "\n" 497 | end 498 | return data 499 | end) 500 | 501 | if fs:sub(-1) == "\n" then 502 | fs = fs:sub(1, -2) 503 | end 504 | end 505 | 506 | if opts.format then 507 | fs = ('%q'):format(fs) 508 | if opts.multiline then 509 | fs = fs:gsub('\\*%]\\\n', function(data) 510 | if ((#data - 1) / 2) % 2 == 1 then 511 | data = data:sub(1, -3) .. '" ..\n"' 512 | end 513 | return data 514 | end) 515 | end 516 | fs = fs:gsub('\\\n', '\\n') 517 | local ok, msg = true, '' 518 | fs = fs:gsub('${([^}]*)}', function(code) 519 | code = assert(deserialize('"' .. code .. '"')):gsub('\\(.)', '%1') 520 | if code:byte(1) == 0x1b then 521 | ok, msg = false, 'Bytecode not permitted in format strings' 522 | elseif ok then 523 | ok, msg = load('return ' .. code) 524 | end 525 | -- This adds markers before and after the code so it can be 526 | -- extracted easily in renderer.import(). 527 | return '" .. minetest.formspec_escape(tostring(--[[${]]' .. code .. 528 | '--[[}]])) .. "' 529 | end) 530 | if not ok then 531 | return nil, msg 532 | end 533 | end 534 | return fs, nil 535 | end 536 | -------------------------------------------------------------------------------- /start-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Start both Python's http.server and sass for testing. 4 | # 5 | 6 | cd $(dirname "$0") || exit 7 | python3 -m http.server & 8 | trap "echo;kill -15 '$!'" EXIT 9 | scss --watch style.scss 10 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @import url("https://fonts.googleapis.com/css?family=Arimo&display=swap"); 3 | .formspec_ast-base > div { 4 | position: absolute; 5 | background-color: #343434; 6 | border: 1px solid #0D0D0D; 7 | border-radius: 3px; 8 | overflow: hidden; 9 | image-rendering: optimizeSpeed; 10 | image-rendering: -moz-crisp-edges; 11 | image-rendering: crisp-edges; 12 | image-rendering: pixelated; } 13 | .formspec_ast-base > div.grid { 14 | background: url("grid.png"); } 15 | .formspec_ast-base > div, .formspec_ast-base > div input, .formspec_ast-base > div textarea, .formspec_ast-base > div select { 16 | font-family: Arimo, sans-serif; 17 | color: #FFFFFF; 18 | font-size: inherit; } 19 | .formspec_ast-base > div > * { 20 | position: absolute; 21 | font-size: 0.34em; 22 | display: block; 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | padding: 0; 27 | text-shadow: 1px 1px black; 28 | white-space: pre; } 29 | .formspec_ast-base img { 30 | -webkit-user-drag: none; 31 | -moz-user-drag: none; 32 | user-drag: none; } 33 | .formspec_ast-base .formspec_ast-clickable:active { 34 | transform: translate(1px, 1px); } 35 | .formspec_ast-base .formspec_ast-label { 36 | margin-top: -0.6em; } 37 | .formspec_ast-base .formspec_ast-label[data-text=""]::after { 38 | content: "(empty label)"; 39 | font-style: italic; } 40 | .formspec_ast-base .formspec_ast-vertlabel { 41 | margin-left: -0.3em; 42 | line-height: 1; } 43 | .formspec_ast-base .formspec_ast-vertlabel[data-text=""]::after { 44 | content: "(\a e\am\ap\at\ay\a \al\a a\a b\a e\al\a)"; 45 | font-style: italic; } 46 | .formspec_ast-base .formspec_ast-box { 47 | background-color: #f002; } 48 | .formspec_ast-base .formspec_ast-button, .formspec_ast-base .formspec_ast-button_exit, 49 | .formspec_ast-base .formspec_ast-image_button[data-drawborder="true"], 50 | .formspec_ast-base .formspec_ast-image_button_exit[data-drawborder="true"], 51 | .formspec_ast-base .formspec_ast-dropdown > div { 52 | box-sizing: border-box; 53 | border: 1px solid #000; 54 | background: #414141; 55 | background: linear-gradient(180deg, #535353 0%, #2E2E2E 100%); 56 | text-align: center; 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | overflow: hidden; } 61 | .formspec_ast-base .formspec_ast-button:hover, .formspec_ast-base .formspec_ast-button_exit:hover, 62 | .formspec_ast-base .formspec_ast-image_button[data-drawborder="true"]:hover, 63 | .formspec_ast-base .formspec_ast-image_button_exit[data-drawborder="true"]:hover, 64 | .formspec_ast-base .formspec_ast-dropdown > div:hover { 65 | background: linear-gradient(180deg, #656565 0%, #383838 100%); } 66 | .formspec_ast-base .formspec_ast-button:active, .formspec_ast-base .formspec_ast-button_exit:active, 67 | .formspec_ast-base .formspec_ast-image_button[data-drawborder="true"]:active, 68 | .formspec_ast-base .formspec_ast-image_button_exit[data-drawborder="true"]:active, 69 | .formspec_ast-base .formspec_ast-dropdown > div:active { 70 | background: linear-gradient(180deg, #464646 0%, #272727 100%); } 71 | .formspec_ast-base .formspec_ast-image_button[data-drawborder="false"], .formspec_ast-base .formspec_ast-image_button_exit[data-drawborder="false"] { 72 | overflow: hidden; 73 | background: none; 74 | border: none; } 75 | .formspec_ast-base .formspec_ast-image_button img, .formspec_ast-base .formspec_ast-image_button_exit img { 76 | width: 100%; 77 | height: 100%; } 78 | .formspec_ast-base .formspec_ast-image_button span, .formspec_ast-base .formspec_ast-image_button_exit span { 79 | position: absolute; 80 | top: 50%; 81 | left: 50%; 82 | transform: translate(-50%, -50%); } 83 | .formspec_ast-base .formspec_ast-field span, .formspec_ast-base .formspec_ast-pwdfield span, .formspec_ast-base .formspec_ast-textarea span { 84 | position: absolute; 85 | transform: translateY(-100%); } 86 | .formspec_ast-base .formspec_ast-field input, .formspec_ast-base .formspec_ast-field textarea, .formspec_ast-base .formspec_ast-pwdfield input, .formspec_ast-base .formspec_ast-pwdfield textarea, .formspec_ast-base .formspec_ast-textarea input, .formspec_ast-base .formspec_ast-textarea textarea { 87 | width: 100%; 88 | height: 100%; 89 | padding-left: 2px; 90 | padding-right: 2px; 91 | box-sizing: border-box; 92 | border: 1px solid #000; 93 | background: #FFFFFF65; 94 | color: white; 95 | text-shadow: 1px 1px black; 96 | font-size: 1em; } 97 | .formspec_ast-base .formspec_ast-field input::selection, .formspec_ast-base .formspec_ast-field textarea::selection, .formspec_ast-base .formspec_ast-pwdfield input::selection, .formspec_ast-base .formspec_ast-pwdfield textarea::selection, .formspec_ast-base .formspec_ast-textarea input::selection, .formspec_ast-base .formspec_ast-textarea textarea::selection { 98 | background: #608631; } 99 | .formspec_ast-base .formspec_ast-field textarea, .formspec_ast-base .formspec_ast-pwdfield textarea, .formspec_ast-base .formspec_ast-textarea textarea { 100 | resize: none; } 101 | .formspec_ast-base .formspec_ast-textarea[data-formspec_ast-name=""] textarea { 102 | border-color: transparent; 103 | background: transparent; } 104 | .formspec_ast-base .formspec_ast-checkbox { 105 | margin-top: -0.6em; } 106 | .formspec_ast-base .formspec_ast-checkbox > div { 107 | width: 1.2em; 108 | height: 1.2em; 109 | margin-right: 0.3em; 110 | box-sizing: border-box; 111 | border: 1px solid #000; 112 | background: #848484; 113 | display: inline-block; 114 | vertical-align: bottom; } 115 | .formspec_ast-base .formspec_ast-checkbox:active > div { 116 | background-color: #608631; } 117 | .formspec_ast-base .formspec_ast-checkbox[data-checked="true"] > div::before { 118 | position: absolute; 119 | content: '✓'; 120 | width: 1.2em; 121 | margin-top: -0.05em; 122 | color: #000; 123 | text-shadow: none; 124 | text-align: center; } 125 | .formspec_ast-base .formspec_ast-list { 126 | font-size: inherit; 127 | overflow: hidden; 128 | border-collapse: collapse; } 129 | .formspec_ast-base .formspec_ast-list td { 130 | width: 1em; 131 | height: 1em; 132 | background: #00000065; 133 | padding: 0; 134 | border: 0.25em solid #343434; } 135 | .formspec_ast-base .formspec_ast-list td:first-child { 136 | border-left: 0; } 137 | .formspec_ast-base .formspec_ast-list td:last-child { 138 | border-right: 0; } 139 | .formspec_ast-base .formspec_ast-list td:hover { 140 | background-color: #FFFFFF30; } 141 | .formspec_ast-base .formspec_ast-list tr:first-child td { 142 | border-top: 0; } 143 | .formspec_ast-base .formspec_ast-list tr:last-child td { 144 | border-bottom: 0; } 145 | .formspec_ast-base .formspec_ast-textlist { 146 | border: 1px solid #1E1E1E; 147 | background: #1E1E1E; 148 | box-sizing: border-box; 149 | overflow-x: hidden; 150 | overflow-y: auto; 151 | font-size: 1em; } 152 | .formspec_ast-base .formspec_ast-textlist > div { 153 | font-size: 0.34em; 154 | padding: 2px; 155 | padding-left: 5px; 156 | padding-right: 5px; } 157 | .formspec_ast-base .formspec_ast-dropdown { 158 | border: 2px solid #1E1E1E; 159 | box-sizing: border-box; } 160 | .formspec_ast-base .formspec_ast-dropdown select { 161 | border: none; 162 | border-radius: 0; 163 | background: #1E1E1E; 164 | width: 100%; 165 | height: 100%; 166 | -moz-appearance: none; 167 | -webkit-appearance: none; 168 | appearance: none; 169 | outline: none; } 170 | .formspec_ast-base .formspec_ast-dropdown select:-moz-focusring { 171 | color: transparent; 172 | text-shadow: 0 0 0 #fff; } 173 | .formspec_ast-base .formspec_ast-dropdown select:focus { 174 | background: #467832; } 175 | .formspec_ast-base .formspec_ast-dropdown.formspec_ast-clickable select { 176 | pointer-events: none; } 177 | .formspec_ast-base .formspec_ast-dropdown > div { 178 | position: absolute; 179 | top: 0; 180 | right: 0; 181 | width: 15px; 182 | height: 100%; 183 | pointer-events: none; } 184 | .formspec_ast-base .formspec_ast-dropdown > div > div { 185 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAECAMAAAB1GNVPAAAAElBMVEU+Pj4/Pz9AQEAVFRUWFhb///+2UyocAAAAA3RSTlMAAAD6dsTeAAAAHUlEQVR42mNggQAGJjDFxMDIyMzMzMjIAATMQAwABbgARWfCNlIAAAAASUVORK5CYII="); 186 | width: 7px; 187 | height: 4px; } 188 | 189 | #formspec_ast-properties { 190 | position: fixed; 191 | top: 0; 192 | right: 0; 193 | height: 100%; 194 | overflow-y: auto; 195 | padding: 10px; } 196 | 197 | #formspec_ast-new { 198 | position: fixed; 199 | top: 0; 200 | left: 0; 201 | height: 100%; 202 | overflow-y: auto; 203 | padding: 10px; } 204 | 205 | /*# sourceMappingURL=style.css.map */ 206 | -------------------------------------------------------------------------------- /style.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Arimo&display=swap'); 2 | 3 | // Web-based formspec editor stylesheets. 4 | // 5 | // © 2020 by luk3yx. 6 | // 7 | // This program is free software: you can redistribute it and/or modify 8 | // it under the terms of the GNU Affero General Public License as 9 | // published by the Free Software Foundation, either version 3 of the 10 | // License, or (at your option) any later version. 11 | // 12 | // This program is distributed in the hope that it will be useful, 13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | // GNU Affero General Public License for more details. 16 | // 17 | // You should have received a copy of the GNU Affero General Public License 18 | // along with this program. If not, see . 19 | 20 | .formspec_ast-base { 21 | & > div { 22 | position: absolute; 23 | background-color: #343434; 24 | border: 1px solid #0D0D0D; 25 | border-radius: 3px; 26 | overflow: hidden; 27 | image-rendering: optimizeSpeed; 28 | image-rendering: -moz-crisp-edges; 29 | image-rendering: crisp-edges; 30 | image-rendering: pixelated; 31 | 32 | &.grid { 33 | background: url("grid.png"); 34 | } 35 | 36 | &, input, textarea, select { 37 | font-family: Arimo, sans-serif; 38 | color: #FFFFFF; 39 | font-size: inherit; 40 | } 41 | 42 | & > * { 43 | position: absolute; 44 | font-size: 0.34em; 45 | display: block; 46 | -webkit-user-select: none; 47 | -moz-user-select: none; 48 | user-select: none; 49 | padding: 0; 50 | text-shadow: 1px 1px black; 51 | white-space: pre; 52 | } 53 | } 54 | 55 | img { 56 | -webkit-user-drag: none; 57 | -moz-user-drag: none; 58 | user-drag: none; 59 | } 60 | 61 | .formspec_ast-clickable { 62 | &:active { 63 | transform: translate(1px, 1px); 64 | } 65 | } 66 | 67 | .formspec_ast-label { 68 | margin-top: -0.6em; 69 | 70 | &[data-text=""]::after { 71 | content: "(empty label)"; 72 | font-style: italic; 73 | } 74 | } 75 | 76 | .formspec_ast-vertlabel { 77 | margin-left: -0.3em; 78 | line-height: 1; 79 | 80 | &[data-text=""]::after { 81 | content: "(\a e\am\ap\at\ay\a \al\a a\a b\a e\al\a)"; 82 | font-style: italic; 83 | } 84 | } 85 | 86 | // Make boxes partially visible 87 | .formspec_ast-box { 88 | background-color: #f002; 89 | } 90 | 91 | 92 | .formspec_ast-button, .formspec_ast-button_exit, 93 | .formspec_ast-image_button[data-drawborder="true"], 94 | .formspec_ast-image_button_exit[data-drawborder="true"], 95 | .formspec_ast-dropdown > div { 96 | box-sizing: border-box; 97 | border: 1px solid #000; 98 | background: #414141; 99 | background: linear-gradient(180deg, #535353 0%, #2E2E2E 100%); 100 | text-align: center; 101 | display: flex; 102 | align-items: center; 103 | justify-content: center; 104 | overflow: hidden; 105 | 106 | &:hover { 107 | background: linear-gradient(180deg, #656565 0%, #383838 100%); 108 | } 109 | &:active { 110 | background: linear-gradient(180deg, #464646 0%, #272727 100%); 111 | } 112 | } 113 | 114 | .formspec_ast-image_button, .formspec_ast-image_button_exit { 115 | &[data-drawborder="false"] { 116 | overflow: hidden; 117 | background: none; 118 | border: none; 119 | } 120 | img { 121 | width: 100%; 122 | height: 100%; 123 | } 124 | span { 125 | position: absolute; 126 | top: 50%; 127 | left: 50%; 128 | transform: translate(-50%, -50%); 129 | } 130 | } 131 | 132 | .formspec_ast-field, .formspec_ast-pwdfield, .formspec_ast-textarea { 133 | span { 134 | position: absolute; 135 | transform: translateY(-100%); 136 | } 137 | input, textarea { 138 | width: 100%; 139 | height: 100%; 140 | padding-left: 2px; 141 | padding-right: 2px; 142 | box-sizing: border-box; 143 | border: 1px solid #000; 144 | background: #FFFFFF65; 145 | color: white; 146 | text-shadow: 1px 1px black; 147 | font-size: 1em; 148 | 149 | &::selection { 150 | background: #608631; 151 | } 152 | } 153 | textarea { 154 | resize: none; 155 | } 156 | } 157 | 158 | .formspec_ast-textarea[data-formspec_ast-name=""] textarea { 159 | border-color: transparent; 160 | background: transparent; 161 | } 162 | 163 | .formspec_ast-checkbox { 164 | margin-top: -0.6em; 165 | & > div { 166 | width: 1.2em; 167 | height: 1.2em; 168 | margin-right: 0.3em; 169 | box-sizing: border-box; 170 | border: 1px solid #000; 171 | background: #848484; 172 | display: inline-block; 173 | vertical-align: bottom; 174 | } 175 | 176 | &:active > div { 177 | background-color: #608631; 178 | } 179 | 180 | &[data-checked="true"] > div::before { 181 | position: absolute; 182 | content: '✓'; 183 | width: 1.2em; 184 | margin-top: -0.05em; 185 | color: #000; 186 | text-shadow: none; 187 | text-align: center; 188 | } 189 | } 190 | 191 | .formspec_ast-list { 192 | font-size: inherit; 193 | overflow: hidden; 194 | border-collapse: collapse; 195 | td { 196 | width: 1em; 197 | height: 1em; 198 | background: #00000065; 199 | padding: 0; 200 | border: 0.25em solid #343434; 201 | &:first-child { 202 | border-left: 0; 203 | } 204 | &:last-child { 205 | border-right: 0; 206 | } 207 | &:hover { 208 | background-color: #FFFFFF30; 209 | } 210 | } 211 | tr:first-child td { 212 | border-top: 0; 213 | } 214 | tr:last-child td { 215 | border-bottom: 0; 216 | } 217 | } 218 | 219 | .formspec_ast-textlist { 220 | border: 1px solid #1E1E1E; 221 | background: #1E1E1E; 222 | box-sizing: border-box; 223 | overflow-x: hidden; 224 | overflow-y: auto; 225 | font-size: 1em; 226 | 227 | & > div { 228 | font-size: 0.34em; 229 | padding: 2px; // TODO: Use em 230 | padding-left: 5px; 231 | padding-right: 5px; 232 | } 233 | } 234 | 235 | .formspec_ast-dropdown { 236 | border: 2px solid #1E1E1E; 237 | box-sizing: border-box; 238 | 239 | select { 240 | border: none; 241 | border-radius: 0; 242 | background: #1E1E1E; 243 | width: 100%; 244 | height: 100%; 245 | -moz-appearance: none; 246 | -webkit-appearance: none; 247 | appearance: none; 248 | 249 | // Remove the outline 250 | outline: none; 251 | &:-moz-focusring { 252 | color: transparent; 253 | text-shadow: 0 0 0 #fff; 254 | } 255 | 256 | &:focus { 257 | background: #467832; 258 | } 259 | } 260 | 261 | &.formspec_ast-clickable select { 262 | pointer-events: none; 263 | } 264 | 265 | & > div { 266 | position: absolute; 267 | top: 0; 268 | right: 0; 269 | width: 15px; 270 | height: 100%; 271 | pointer-events: none; 272 | 273 | // The arrow icon 274 | & > div { 275 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAECAMAAAB1GNVPAAAAElBMVEU+Pj4/Pz9AQEAVFRUWFhb///+2UyocAAAAA3RSTlMAAAD6dsTeAAAAHUlEQVR42mNggQAGJjDFxMDIyMzMzMjIAATMQAwABbgARWfCNlIAAAAASUVORK5CYII="); 276 | width: 7px; 277 | height: 4px; 278 | } 279 | } 280 | } 281 | 282 | } 283 | 284 | #formspec_ast-properties { 285 | position: fixed; 286 | top: 0; 287 | right: 0; 288 | height: 100%; 289 | overflow-y: auto; 290 | padding: 10px; 291 | } 292 | 293 | #formspec_ast-new { 294 | position: fixed; 295 | top: 0; 296 | left: 0; 297 | height: 100%; 298 | overflow-y: auto; 299 | padding: 10px; 300 | } 301 | --------------------------------------------------------------------------------