├── .gitignore ├── LICENSE ├── MIT ├── MIT-0 ├── README.md ├── docs ├── cal.png ├── chat.png ├── demos │ ├── dialog │ │ ├── always.html │ │ ├── index.html │ │ └── once.html │ ├── edit │ │ ├── field.html │ │ └── index.html │ ├── hello │ │ ├── greeting.html │ │ └── index.html │ └── tabs │ │ ├── cat.html │ │ ├── dog.html │ │ ├── horse.html │ │ └── index.html ├── extensions │ ├── index.html │ ├── loader │ │ ├── content.html │ │ └── index.html │ ├── multitarget │ │ ├── content.html │ │ └── index.html │ ├── no-history │ │ ├── cat.html │ │ ├── dog.html │ │ └── index.html │ ├── repeat-gets │ │ ├── content.html │ │ └── index.html │ ├── swap-template │ │ ├── add.html │ │ ├── delete.html │ │ └── index.html │ └── unwrap-template │ │ ├── index.html │ │ └── rows.html ├── favicon.png ├── footer.js ├── index.html ├── style.css └── todo.png ├── examples ├── cf_clean_target_tabs │ ├── cat.html │ ├── dog.html │ ├── horse.html │ ├── index.html │ ├── style.css │ ├── worker.js │ └── wrangler.toml ├── html_clean_target_tabs │ ├── cat.html │ ├── dog.html │ ├── horse.html │ ├── index.html │ └── style.css ├── html_tabs │ ├── cat.html │ ├── dog.html │ ├── horse.html │ ├── iframe.html │ ├── index.html │ └── style.css ├── index.html ├── js_simple_error_handling │ ├── cat1.jpg │ ├── cat2.jpg │ ├── cat3.jpg │ ├── index.html │ ├── slide1.html │ ├── slide2.html │ ├── slide3.html │ └── style.css ├── node_chat │ ├── index.html │ ├── index.js │ └── server.js ├── php_calendar │ ├── create_event.php │ ├── date.php │ ├── delete_event.php │ ├── event.php │ ├── index.php │ ├── month.php │ ├── style.css │ └── util │ │ ├── color.php │ │ ├── persist.php │ │ └── props.php ├── php_dialog │ ├── index.php │ ├── picker.php │ └── style.css ├── php_new_tab_detection │ ├── content.php │ ├── footer.php │ ├── index.php │ ├── prelude.php │ └── style.css ├── php_todo │ ├── clear.php │ ├── create.php │ ├── edit.php │ ├── index.php │ └── style.css ├── run_servers.sh └── style.css ├── glitch.json ├── htmz.dev.html ├── htmz.html └── npx ├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | - Files in this repo are MIT licensed (see `/MIT`), except: 2 | - The code in the files `/htmz.html` and `/htmz.dev.html` are licensed under MIT-0 (see `/MIT-0`). No attribution needed to copy these snippets. 3 | -------------------------------------------------------------------------------- /MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 Lean Rada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MIT-0: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2024 Lean Rada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htmz 2 | 3 | _a low power tool for html_ 4 | 5 | htmz is a minimalist HTML microframework that gives you the power to create dynamic web user interfaces with the familiar simplicity of **plain HTML**. 6 | 7 | Zero dependencies. Zero JS bundles to load. Not even a backend is required. _Just an inline HTML snippet_. 8 | 9 | See the [documentation website](https://kalabasa.github.io/htmz) for more details, usage, examples, and more. 10 | 11 | ## Installing 12 | 13 | Simply copy the following snippet into your page. 14 | 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | 21 | ## What does it do? 22 | 23 | htmz does one thing and one thing only. 24 | 25 | - Enable you to load HTML resources within _any element_ in the page. 26 | 27 | Imagine clicking a link, but instead of reloading the whole page, it only updates a relevant portion of the page. Think tabbed UIs, dual-pane list-detail layouts, dialogs, in-place editors, and the like. 28 | 29 | **htmz is a generalisation of HTML frames.** — Load HTML resources within ~~any frame~~ _any element_ in the page. 30 | -------------------------------------------------------------------------------- /docs/cal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/docs/cal.png -------------------------------------------------------------------------------- /docs/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/docs/chat.png -------------------------------------------------------------------------------- /docs/demos/dialog/always.html: -------------------------------------------------------------------------------- 1 |

I now have perpetual access.

-------------------------------------------------------------------------------- /docs/demos/dialog/index.html: -------------------------------------------------------------------------------- 1 |
2 |

HTML

3 |
<base target=htmz>
 5 | 
 6 | <p>Hello, user!</p>
 7 | 
 8 | <!-- Self-closing dialog:
 9 |      The dialog will be replaced by the response
10 |      triggered by the form inside it. -->
11 | <dialog id="content" open>
12 |   <form>
13 |     <p>Would you like to enable access to zlorgonium caches?</p>
14 |     <button
15 |       formaction="/access#content"
16 |       name="enable"
17 |       value="once"
18 |     >
19 |       Just once
20 |     </button>
21 |     <button
22 |       formaction="/access#content"
23 |       name="enable"
24 |       value="always"
25 |     >
26 |       Always
27 |     </button>
28 |     <button formmethod="dialog">
29 |       Never
30 |     </button>
31 |   </form>
32 | </dialog>
34 |

Result

35 |
36 |

Hello, user!

37 | 38 |
39 |

Would you like to enable access to zlorgonium caches?

40 | 47 | 54 | 55 |
56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /docs/demos/dialog/once.html: -------------------------------------------------------------------------------- 1 |

I now have one-time access.

-------------------------------------------------------------------------------- /docs/demos/edit/field.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /docs/demos/edit/index.html: -------------------------------------------------------------------------------- 1 |
2 |

HTML

3 |
4 |
<base target=htmz>
 6 | 
 7 | <!-- Clicking the 'edit' link replaces the static text
 8 |      with an editable field component provided by /field -->
 9 | <label>
10 |   Title:
11 |   <div id="title">
12 |     <b>The Seven Bits</b>
13 |     <a href="/field?id=title&value=The Seven Bits#title">edit</a>
14 |   </div>
15 | </label>
16 | 
17 | <label>
18 |   Genre:
19 |   <div id="genre">
20 |     <b>Horror</b>
21 |     <a href="/field?id=genre&value=Horror#genre">edit</a>
22 |   </div>
23 | </label>
24 | 
25 | <label>
26 |   Description:
27 |   <div id="description">
28 |     <b>Seven ate nine.</b>
29 |     <a href="/field?id=description&value=Seven ate nine.#description">edit</a>
30 |   </div>
31 | </label>
33 |

34 | NodeJS example backend /field 35 |

36 |
// This field component toggles between static / edit mode
38 | // by replacing itself when 'edit' / 'save' is clicked.
39 | if (request.query.save) {
40 |   // Static mode
41 |   response.send(`
42 |     <div id='${request.query.id}'>
43 |       <b>${request.query.value}</b>
44 |       <a href="/field?id=${request.query.id}&value=${request.query.value}#${request.query.id}">
45 |         edit
46 |       </a>
47 |     </div>
48 |   `);
49 | } else {
50 |   // Edit mode
51 |   response.send(`
52 |     <form id="${request.query.id}" action="/field#${request.query.id}">
53 |       <input hidden name="id" value="${request.query.id}">
54 |       <input name="value" value="${request.query.value}">
55 |       <button name="save" value="save">save</button>
56 |     </form>
57 |   `);
58 | }
60 |
61 |

Result

62 |
63 | 72 | 73 | 80 | 81 | 91 |
92 |
93 | -------------------------------------------------------------------------------- /docs/demos/hello/greeting.html: -------------------------------------------------------------------------------- 1 |

2 | Hello, 3 | 6 |

7 | -------------------------------------------------------------------------------- /docs/demos/hello/index.html: -------------------------------------------------------------------------------- 1 |
2 |

HTML

3 |
4 |
<!-- Sends form data to /greeting,
 6 |      putting the response onto #result -->
 7 | <form action="/greeting#result" target=htmz>
 8 |   <label>
 9 |     What’s your name?
10 |     <input name="name">
11 |   </label>
12 |   <button>Greet me</button>
13 | </form>
14 | 
15 | <!-- This will be replaced by /greeting's response -->
16 | <div id="result"></div>
18 |

19 | PHP example backend /greeting 20 |

21 |
<p id="result">
23 |   Hello, <?= $_REQUEST['name'] ?>
24 | </p>
26 |
27 |

Result

28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /docs/demos/tabs/cat.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Felis est animalis elegantissimum et misteriosum. Felis est 4 | quadrupes, sed parvus et gracilis. Habent corpora flexilia et caudam longam. 5 | Oculi eorum sunt acuti et vigilantes, quae adiuvant eos in venatione. Felium 6 | natura est curiosa et independens, saepe explorans loca nova et exspectans 7 | tempus opportunitatis ad venandum. Multae sunt species felium, ab domesticis 8 | ad agrestes, et varietas colorum eorum est mirabilis. In multis culturis, 9 | felis est considerata animal domesticum, adfertque suavitatem et companiam 10 | in domum hominum. 11 |

12 |
13 | -------------------------------------------------------------------------------- /docs/demos/tabs/dog.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Canis est animalis fidelis et amicus hominis. Canes sunt 4 | quadrupedes et pertingunt in multas formas et magnitudines. Exemplum, lupus 5 | est genus canis, sed domesticus canis est proximus amicus hominis. Canis 6 | habet olfactum acutum et auditum bonum, quod facit eum aptum ad custodiendum 7 | et adiuvandum hominem. Multa genera canum existunt, ab parvis et mollibus 8 | usque ad magnos et fortes. In multis culturis, canis est consideratus non 9 | solum animal domesticum sed etiam amicum fidelissimum. 10 |

11 |
12 | -------------------------------------------------------------------------------- /docs/demos/tabs/horse.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Equus est animal nobilissimum et robustum. Equi sunt 4 | quadrupedes, magni et fortes. Habent crura longa et caudam fluam. Equi sunt 5 | celeres et agiles, idonei ad currum et vecturam. Oculi eorum sunt magni et 6 | vivaces, et auribus acutis auditum excellentem habent. Equi fuerunt auro 7 | pugnandi et trahendi plaustra ab antiquis temporibus, et usque ad hodiernum 8 | diem, sunt valde utiles in agris, in cursibus et in equestri exercitatione. 9 | Equi sunt etiam amici fideles hominum, qui saepe sunt asserunt sicut 10 | coniuncti socii in multis laboribus et gaudiis. 11 |

12 |

Hey! Try your browser back button! It works natively!

13 |
14 | -------------------------------------------------------------------------------- /docs/demos/tabs/index.html: -------------------------------------------------------------------------------- 1 |
2 |

HTML

3 |
<base target=htmz>
 5 | 
 6 | <!-- Loads static HTML onto #my-tab-panel -->
 7 | <div role="tablist">
 8 |   <a role="tab" href="/dog.html#my-tab-panel">Dog</a> |
 9 |   <a role="tab" href="/cat.html#my-tab-panel">Cat</a> |
10 |   <a role="tab" href="/horse.html#my-tab-panel">Horse</a>
11 | </div>
12 | 
13 | <!-- This will be replaced by the loaded HTML -->
14 | <div id="my-tab-panel" role="tabpanel"></div>
16 |

Result

17 |
18 |
19 | Dog | 20 | Cat | 21 | Horse 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /docs/extensions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | htmz/extensions 5 | 6 | 7 | 11 | 15 | 16 | 87 | 88 | 127 | 128 | 129 |
130 |

131 | =htmz>
136 |  <extensions> 140 | 🍱 141 |

142 | back to main page 143 |
144 | 145 |
146 | 170 | 171 |
172 |
173 | 174 |

Demo

175 | 181 | 182 |

Code

183 |
184 |
185 | No extension selected! 186 |
187 |
188 |
189 |
190 | 191 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/extensions/loader/content.html: -------------------------------------------------------------------------------- 1 |

Not sorry to keep you waiting.

2 | 3 | -------------------------------------------------------------------------------- /docs/extensions/loader/index.html: -------------------------------------------------------------------------------- 1 |
2 | 26 | 27 |
28 | 29 |
30 |

loader

31 |

32 | This extension applies a .loader CSS class to the target 33 | element when a request starts, to indicate loading state. 34 |

35 |
36 | 37 |
38 | Load content 39 |
40 | 41 | 64 | -------------------------------------------------------------------------------- /docs/extensions/multitarget/content.html: -------------------------------------------------------------------------------- 1 |

2 | Operation successful! 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Hoc est exemplum 4 | multitarget, vel "update extra ordinem" ut vocant. 5 |

6 | -------------------------------------------------------------------------------- /docs/extensions/multitarget/index.html: -------------------------------------------------------------------------------- 1 |
2 | 28 | 29 |
30 | 31 |
32 |

multitarget

33 |

34 | This extension allows you to target multiple elements in a single request. 35 |

36 |
37 | 38 | Load content 39 |
40 |
41 | 42 | 58 | -------------------------------------------------------------------------------- /docs/extensions/no-history/cat.html: -------------------------------------------------------------------------------- 1 |
2 | Felis est animalis elegantissimum et misteriosum. Felis est quadrupes, sed parvus et gracilis. Habent corpora flexilia et caudam longam. Oculi eorum sunt acuti et vigilantes, quae adiuvant eos in venatione. Felium natura est curiosa et independens, saepe explorans loca nova et exspectans tempus opportunitatis ad venandum. Multae sunt species felium, ab domesticis ad agrestes, et varietas colorum eorum est mirabilis. In multis culturis, felis est considerata animal domesticum, adfertque suavitatem et companiam in domum hominum. 3 |
-------------------------------------------------------------------------------- /docs/extensions/no-history/dog.html: -------------------------------------------------------------------------------- 1 |
2 | Canis est animalis fidelis et amicus hominis. Canes sunt quadrupedes et pertingunt in multas formas et magnitudines. Exemplum, lupus est genus canis, sed domesticus canis est proximus amicus hominis. Canis habet olfactum acutum et auditum bonum, quod facit eum aptum ad custodiendum et adiuvandum hominem. Multa genera canum existunt, ab parvis et mollibus usque ad magnos et fortes. In multis culturis, canis est consideratus non solum animal domesticum sed etiam amicum fidelissimum. 3 |
-------------------------------------------------------------------------------- /docs/extensions/no-history/index.html: -------------------------------------------------------------------------------- 1 |
2 | 23 | 24 |
25 | 26 |
27 |

no-history

28 |

29 | This extension prevents history entries from being made for htmz requests. 30 | 31 | [HTML proposal: 32 | whatwg/html#6501] 35 | 36 |

37 |
38 | 39 | Load dog. 40 | Load cat. 41 |
42 | -------------------------------------------------------------------------------- /docs/extensions/repeat-gets/content.html: -------------------------------------------------------------------------------- 1 |

2 | The time now is 3 | 6 | . 7 |

8 | -------------------------------------------------------------------------------- /docs/extensions/repeat-gets/index.html: -------------------------------------------------------------------------------- 1 |
2 | 20 | 21 |
22 | 23 |
24 |

repeat-gets

25 |

26 | Normally, browsers cache GET requests to the same URL in an iframe. This 27 | extension allows you to skip that behavior and make repeated GET requests to 28 | the same URL. 29 |

30 |
31 | 32 | Load content 33 |
34 | -------------------------------------------------------------------------------- /docs/extensions/swap-template/add.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/extensions/swap-template/delete.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/extensions/swap-template/index.html: -------------------------------------------------------------------------------- 1 |
2 | 47 | 48 |
49 | 50 |
51 |

swap-template

52 |

contributed by yuretz

53 |

54 | Sometimes it would be nice to be able to preserve the target element 55 | or its children in the DOM tree. This extension allows you to do just that by using HTML 56 | fragments wrapped in 57 | <template> 62 | tags and 63 | <slot> elements within them. 68 |

69 |
<slot>
Represents the original element as is
70 |
<slot>...some new content</slot>
Represents the original element 71 | with its children replaced with some new content
72 |
<slot name="children">
Represents the original element child nodes
73 |
74 |

75 |

76 | The example below demonstrates a simple editable list implemented using this extension features. 77 |

78 |
79 | 80 |
81 | 87 |
88 | 89 | 94 | -------------------------------------------------------------------------------- /docs/extensions/unwrap-template/index.html: -------------------------------------------------------------------------------- 1 |
2 | 22 | 23 |
24 | 25 |
26 |

unwrap-template

27 |

28 | Certain HTML elements have restrictions on what type of parent element they 29 | can be in. For example, a <tr> requires a 30 | <table> parent [or a t(head|body|foot)]. Simply 31 | sending tr on its own is illegal. 32 |

33 |

34 | The native solution to represent HTML fragments is the 35 | <template> 40 | tag. Elements that would otherwise be illegal on their own can be parsed 41 | legally inside a template tag. 42 |

43 |

44 | This extension facilitates the above approach by unwrapping a top-level 45 | template element in the response. 46 |

47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 |
FooBar
59 | Load rows 60 |
64 | 65 | 72 | -------------------------------------------------------------------------------- /docs/extensions/unwrap-template/rows.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/docs/favicon.png -------------------------------------------------------------------------------- /docs/footer.js: -------------------------------------------------------------------------------- 1 | // Footer animation generates fake URLs and fake containers 2 | 3 | // prettier-ignore 4 | const resources = 5 | "user,post,comment,product,order,file,location,event,task,invoice,employee,customer,review,article,note,song,album,booking,reservation,ticket,payment,transaction,message,conversation,tag,recipe,contact,subscription,profile,group,document,question,answer,notification,reminder,thread,book,project,folder".split(","); 6 | const pathExts = ",,,,,,.html,.php,.asp,.jsp".split(","); 7 | const listQueries = ",,page".split(","); 8 | const singleQueries = ",%,%id,id,/".split(","); 9 | const listUINames = "list,grid,table".split(","); 10 | // prettier-ignore 11 | const singleUINames = 12 | ",container,row,item,box,overlay,preview,form,tooltip,toast,chip,tooltip,tabview,dropdown,card,modal,pane,panel,tabpanel,dataview,view".split(","); 13 | 14 | const footer = document.querySelector(".footer"); 15 | const footerSupports = footer.querySelectorAll("& > .support"); 16 | 17 | const animate = async () => { 18 | footerSupports[0].textContent = ""; 19 | footerSupports[0].classList.remove("entering"); 20 | footerSupports[0].classList.add("exiting"); 21 | footer.classList.add("idle"); 22 | footerSupports[1].textContent = ""; 23 | footerSupports[1].classList.remove("entering"); 24 | footerSupports[1].classList.add("exiting"); 25 | await wait(200); 26 | 27 | const plural = Math.random() < 0.5; 28 | 29 | const resource = randomItemFrom(resources); 30 | 31 | const resourcePath = resource + (plural ? "s" : ""); 32 | const pathExt = randomItemFrom(pathExts); 33 | const method = pathExt ? "GET" : randomItemFrom(["GET", "POST"]); 34 | let pathSuffix = 35 | pathExt !== ".html" 36 | ? plural 37 | ? randomItemFrom(listQueries) 38 | : randomItemFrom(singleQueries) 39 | : ""; 40 | if (pathSuffix) { 41 | const int = 10 + Math.floor(Math.random() * 90); 42 | if (plural) { 43 | pathSuffix = `?${pathSuffix}=${int}`; 44 | } else { 45 | if (pathSuffix.endsWith("/")) { 46 | pathSuffix = pathSuffix + int; 47 | } else { 48 | pathSuffix = `?${pathSuffix.replace(/%/g, resource[0])}=${int}`; 49 | } 50 | } 51 | } 52 | 53 | const uiName = plural 54 | ? randomItemFrom(listUINames) 55 | : randomItemFrom(singleUINames); 56 | const separator = Math.random() < 0.33 ? "" : Math.random() < 0.5 ? "_" : "-"; 57 | const formattedUIName = uiName && (separator ? uiName : uiName[0].toUpperCase() + uiName.slice(1)); 58 | 59 | const resourceText = `${method} /${resourcePath}${pathExt}${pathSuffix}`; 60 | const containerText = `#${resource}${uiName && separator}${formattedUIName}`; 61 | 62 | footerSupports[0].textContent = resourceText; 63 | footerSupports[0].classList.add("entering"); 64 | footerSupports[0].classList.remove("exiting"); 65 | await wait(400); 66 | footer.classList.remove("idle"); 67 | await wait(100); 68 | footerSupports[1].textContent = containerText; 69 | footerSupports[1].classList.add("entering"); 70 | footerSupports[1].classList.remove("exiting"); 71 | await wait(1600); 72 | animate(); 73 | }; 74 | 75 | animate(); 76 | 77 | function randomItemFrom(list) { 78 | return list[Math.floor(Math.random() * list.length)]; 79 | } 80 | 81 | function wait(ms) { 82 | return new Promise((resolve) => setTimeout(resolve, ms)); 83 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | htmz - a low power tool for html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | =>htmz> 18 |

19 | a low power tool for html 20 | 21 |
22 |

23 | htmz is a minimalist HTML microframework for creating interactive 24 | and modular web user interfaces with the familiar simplicity of 25 | plain HTML. 26 | [GitHub] 27 |

28 |
29 | 30 |
31 |
32 |

plain🍦

33 |

34 | Use straight up HTML. No supersets. No hz- ng- hx- v- w- x-; no 35 | special attributes. No DSLs. No <custom-elements>. 36 | Just vanilla HTML. 37 |

38 |
39 |
40 |

lightweight🪶

41 |

42 | 166 bytes in total. Zero dependencies. Zero JS 43 | bundles to load. Not even a backend is required. 44 | Just an inline HTML snippet. 45 |

46 |
47 |
48 |

nofilter

49 |

50 | No preventDefaults. No hidden layers. Real DOM, real interactions. No 51 | VDOM, no click listeners. No AJAX, no fetch. 52 | No reinventing browsers. 53 |

54 |
55 |
56 | 57 |
58 |

59 | In a nutshell, htmz lets you swap page fragments on request using 60 | vanilla HTML. 61 |

62 |

63 | Imagine clicking a link, but instead of reloading the whole page, it 64 | only updates the relevant portion of the page. 65 |

66 |

67 | htmz is an experiment inspired by 68 | htmx, 69 | Comet, ‘HTML As The Engine Of Application State’[1][2], and 78 | other similar web application architectures. 79 |

80 |
81 | 82 |
83 |

Demos

84 |

Check out these demos to get an idea of what htmz can do!

85 |
86 | Tabs 89 | Greeting 95 | Edit inline 98 | Dialog 104 | More examples# 107 |
108 |
109 |
🐙 Select an example above!
110 |
111 |

112 | See also: 113 | Extensions 🍱 114 |

115 |
116 | 117 |
118 |

Installing

119 |

Simply copy the following snippet into your page:

120 |
<iframe hidden name=htmz onload="setTimeout(()=>document.querySelector(contentWindow.location.hash||null)?.replaceWith(...contentDocument.body.childNodes))"></iframe>
123 |

124 | For npm enjoyers, use the following npm commands to 125 | automate the simple process of copying the snippet. For maximum 126 | npm enjoyment, this npm package contains 25 bonus dependencies! 127 |

128 |
npm install --save-dev htmz
130 | npx htmzify ./path/to/my/index.html
132 |

133 | For hackers, you may start with the development version 134 | (deminified): 135 | htmz.dev.html 140 |

141 |
142 | 143 |
144 |

Basic usage

145 |

146 | To invoke htmz, you need a hyperlink (or form) having these attributes: 147 |

148 |
    149 |
  1. 150 | href (or action) pointing to the 151 | resource URL 152 | href="/flower.html 157 |
  2. 158 |
  3. 159 | Continuing within the href: 160 | destination ID selector 161 | #my-element" 164 |
  4. 165 |
  5. 166 | And a target attribute with this value 167 | target=htmz 170 |
  6. 171 |
172 |
<!-- Loads /flower.html onto #my-element -->
174 | <a href="/flower.html#my-element" target=htmz>Flower</a>
176 |

177 | While this looks like an abuse of the URL fragment (it is), there is no 178 | other use for the URL fragment in this context, so it was repurposed as 179 | the destination ID selector. And it already looks like a CSS ID 180 | selector. 181 |

182 |

183 | ⚠ Important note: The loaded content 184 | replaces the selected destination. It may not be 185 | intuitive at first, but htmz does not insert the content into 186 | the destination. The rationale is that replacement is a more powerful 187 | operation. With replacement, you can replace, 188 | delete (replace with nothing), and insert-into (replace 189 | with the same container as original). 190 |

191 |
192 | 193 |
194 |

What does it do exactly?

195 |

htmz does one thing and one thing only.

196 |

197 | 198 | Load HTML onto any element in the page on request. 199 | 200 |

201 |

202 | Think tabbed UIs, dual-pane list-detail layouts, dialogs, in-place 203 | editors, and the like. 204 |

205 |

206 | This idea is not new. Dividing web pages into independently 207 | reloading parts has been a thing since mid-1990s. They were called 208 | frames, namely, <iframe>s, <frame>s, and <frameset>s. 213 |

214 |

215 | htmz is a generalisation of HTML frames. — Load 216 | HTML resources within any frame any element in the 217 | page. 218 |

219 |

220 | Read more on how it works in a section 221 | below. 222 |

223 |
224 | 225 |
226 |

Examples

227 |
228 | todo app 229 | chat app 230 | calendar app 231 |
232 |

233 | More example applications, componentization approaches, and code in 234 | different languages can be found in the 235 | /examples directory. To start the example server: 236 |

237 |
cd examples
239 | ./run_servers.sh
241 |

Then load http://localhost:3000/.

242 |
243 | 244 |
245 |

Advanced usage

246 |

247 | Naturally, only <a> and <form> elements 248 | can target and invoke htmz (as of current HTML5). This is fine; it’s 249 | semantic, after all. However, HTML offers a couple more features that 250 | work well with htmz. 251 |

252 | 253 |
254 |

Per-button action & target

255 |

256 | If you want to override the form’s action on a per-button basis, use 257 | the 258 | <button>’s 259 | formaction 264 | attribute. 265 |

266 |
<form action="/default#my-target" target=htmz>
268 |   <button>Default form action</button>
269 |   <button formaction="/button#my-target">
270 |     Different button action
271 |   </button>
272 |   <button formaction="/another-action#another-target">
273 |     Another action
274 |   </button>
275 | </form>
277 |
278 | 279 |
280 |

Base target value

281 |

Tired of adding target=htmz to every link and form?

282 |

283 | Using the 284 | base 289 | element, set htmz as the default target for all relative links. Add 290 | this at the top of your page. 291 |

292 |
<base target=htmz>
295 |
296 | 297 |
298 |

Clean target values

299 |

300 | Don’t like the look of target=htmz at all? Prefer using 301 | the real target as the value? 302 |

303 |

304 | We can do a hack that enables you to write the target ID selector in 305 | the target attribute itself! Like this: 306 |

307 |
<!-- Loads /flower.html onto #my-element -->
309 | <a href="/flower.html" target="#my-element">Flower</a>
311 |

312 | The key is to add an iframe with a matching name, and modify 313 | the htmz snippet accordingly. 314 |

315 |
<iframe hidden name="#my-element" onload="htmz(this)"></iframe>
317 | <script>
318 |   function htmz(frame) {
319 |     document.querySelector(frame.name) // use the iframe's name instead of the URL hash
320 |       ?.replaceWith(...frame.contentDocument.body.childNodes);
321 |   }
322 | </script>
323 | 
325 |

326 | You can even 327 | automate the generation of matching target iframes. 332 |

333 |
334 | 335 |
336 |

Support opening links in a new tab

337 |

338 | What if the user opens an htmz link in a new tab? Well, they would be 339 | loading your page fragment, carefully designed to be injected into an 340 | exsting page, on its own! 341 |

342 |

343 | Using the 344 | Sec-Fetch-Dest 349 | header, you can 350 | fall back to a full page 355 | in these cases. This header lets the server know the request’s 356 | destination and render either a fragment or a full page 357 | appropriately. 358 |

359 |
360 | 361 |
362 |

Scripting / interactivity

363 |

364 | If you need something more interactive than the request-response 365 | model, you may try the htmz companion scripting language: 366 | javazcript. Sorry, I meant JavaScript, a scripting language 367 | designed to make HTML interactive. 368 |

369 |

370 | htmz does not preclude you writing JS or using UI libraries to enhance 371 | interaction. You could, say, enhance a single form control with 372 | vanillaJS, but 373 | the form 374 | values 375 | could still be submitted as a regular HTTP form with htmz. 376 |

377 |

That said, htmz is extensible!

378 |
379 |
380 | 381 |
382 |

Extensibility

383 |

384 | Need advanced selectors? Need error handling? Multiple targets? Fear 385 | not; the hero is here to save the day. The hero is you. 386 |

387 |

388 | Here’s 389 | the development version of the snippet. Feel free to hack and extend according to your needs. You’re a 394 | programmer, right? 395 |

396 |
<script>
398 |   function htmz(frame) {
399 |     // Write your extensions here
400 | 
401 |     // Remove setTimeout to let the browser autoscroll content changes into view
402 |     setTimeout(() =>
403 |       document
404 |         .querySelector(frame.contentWindow.location.hash || null)
405 |         ?.replaceWith(...frame.contentDocument.body.childNodes)
406 |     );
407 |   }
408 | </script>
409 | <iframe hidden name=htmz onload="htmz(this)"></iframe>
411 |

412 | 413 | Pre-written extensions are now available in the 414 | Extensions 🍱 page. 415 | 416 |

417 |
418 | 419 |
420 |

FAQ

421 | 422 |
423 |

How does it work?

424 |

425 | htmz is an iframe named "htmz". You invoke htmz by loading a 426 | URL into the iframe via target=htmz. By using an iframe, we lean on 427 | the browser’s native capability to fetch the URL and parse the HTML. 428 | After loading the HTML resource, we take the resulting DOM via an 429 | onload handler. 430 |

431 |

htmz is essentially a proxy target.

432 |

433 | Like how a proxy server forwards requests to some specified server, 434 | proxy target htmz forwards responses into some specified target. 435 |

436 |
<!-- The ideal:
438 |      GET /flower.html => #my-element            -->
439 | <a href="/flower.html" target="#my-element">Flower</a>
440 | 
441 | <!-- Actual:
442 |      GET /flower.html =htmz> #my-element        -->
443 | <a href="/flower.html#my-element" target=htmz>Flower</a>
445 |

446 | When you load a URL into the htmz iframe, the onload handler kicks in. 447 | It extracts your destination ID selector from the URL hash fragment 448 | and transplants the iframe’s contents (now containing the loaded HTML 449 | resource) into your specified destination. 450 |

451 |

452 | htmz only runs when you invoke it. It does not continually parse your 453 | DOM and scan it for special attributes or syntax, nor does it attach 454 | listeners in your DOM. It’s a proxy not a VPN. 455 |

456 |
457 | 458 |
459 |

So it’s just another JavaScript framework?

460 |

Oh my! Not the f-word!!!

461 |

462 | On a more serious note, I would say that rather than a JS one, it’s 463 | more of an HTML micro-f*******k. It does use JS, but only the minimum 464 | necessary. 465 |

466 |
467 | 468 |
469 |

Is htmz a library or a framework?

470 |

htmz is a snippet. ✂️

471 |
472 | 473 |
474 |

What does htmz mean?

475 |

476 | HTMZ stands for 477 | 478 | Html with Targeted Manipulation Zones. 479 | 480 |

481 |
482 | 483 |
484 |

Is this a joke?

485 |

486 | This started as a 487 | 488 | “Do I really need htmx? Can’t I do the load-link-into-target thing 489 | with current web? Sounds a lot like frames.” 490 | 491 | and ended up with this. 492 |

493 |

494 | So, it isn’t quite a joke, but a response to htmx. I wanted to try 495 | htmx. The premise sounded great (Why should you only be able to replace the entire screen?), then I saw that it was 16kB of JavaScript. Huh. Then there’s 498 | special syntax everywhere. Huh. I don’t want to learn a whole new set 499 | of instructions and Turing-complete DSLs specific to those 500 | instructions. 501 |

502 |

503 | Regardless of joke status, htmz seems fine as a library. It feels 504 | kinda powerful for its tiny size. (But really it’s the browser that’s 505 | doing the heavy lifting!) Nonetheless, there are limitations. 506 |

507 |
508 | 509 |
510 |

What are the limitations?

511 |

512 | The main direct limitation is having only one destination per 513 | response. However, this can be fixed by writing an extension. ;) 514 |

515 |

516 | A more general but classic limitation is the request-response model. 517 | The Web 1.0 model, and the baggage that comes with it. Like a 518 | roundtrip delay on every interaction, a browser history entry on every 519 | click, etc. 520 |

521 |

522 | The Web 1.0 model might also mean putting more UI logic in the web 523 | server. This can be a good thing or a bad thing, as it could lead to 524 | either consolidation or fragmentation of UI logic, which respectively 525 | decreases or increases complexity. It really depends on your goal and 526 | style. 527 |

528 |

529 | Lastly, this is not a templating library! No HTML imports or 530 | client-side includes. 531 |

532 |
533 |
534 | 535 | 542 | 543 | 544 | 545 | 546 | 547 | 551 | 552 | 553 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, 3 | segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, 4 | sans-serif; 5 | margin: 5vmin auto; 6 | padding: 0 1rem; 7 | max-width: 80rem; 8 | font-size: 1.034rem; 9 | line-height: 1.6; 10 | background: #f0f4f8; 11 | color: #237; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | .footer, 19 | h1, 20 | h2, 21 | h3 { 22 | font-weight: bold; 23 | font-family: "Courier New", Courier, monospace; 24 | } 25 | h1 { 26 | font-size: 2rem; 27 | } 28 | h2 { 29 | font-size: 1.2rem; 30 | padding-inline-start: 0.5rem; 31 | border-inline-start: solid 0.25rem currentColor; 32 | } 33 | h3 { 34 | font-size: 1.1rem; 35 | padding-inline-start: 0.5rem; 36 | border-inline-start: solid 0.125rem currentColor; 37 | } 38 | 39 | .stronger { 40 | color: #d40; 41 | } 42 | 43 | p, 44 | ol, 45 | ul, 46 | .code { 47 | max-width: 40rem; 48 | } 49 | 50 | section { 51 | margin: 4rem 0; 52 | } 53 | @media (max-width: 60rem) { 54 | section { 55 | margin: 3rem 0; 56 | } 57 | } 58 | section section { 59 | margin: 2rem 0; 60 | } 61 | section:has(:target):not(:has(section :target)) { 62 | animation: flash 4s ease-in backwards; 63 | } 64 | @keyframes flash { 65 | from { 66 | background-image: linear-gradient(to right, #fec, transparent 50%); 67 | background-size: 200% 100%; 68 | background-position: 0 0; 69 | } 70 | to { 71 | background-image: linear-gradient(to right, #fec, transparent 50%); 72 | background-size: 200% 100%; 73 | background-position: 100% 0; 74 | } 75 | } 76 | 77 | .title-decor { 78 | font-variant-ligatures: none; 79 | opacity: 0.6; 80 | color: #0bd; 81 | } 82 | 83 | .title { 84 | text-align: center; 85 | margin-bottom: 0; 86 | } 87 | .title .title-decor { 88 | display: inline-block; 89 | width: 2ch; 90 | text-align: start; 91 | } 92 | .title-arrow { 93 | transform: translateX(50%); 94 | clip-path: inset(0 50% 0 0); 95 | animation: titleArrow 0.4s steps(4); 96 | } 97 | @keyframes titleArrow { 98 | from { 99 | transform: translateX(-50%); 100 | clip-path: inset(0 -50% 0 0); 101 | } 102 | } 103 | .title-arrowhead { 104 | clip-path: inset(0 0 0 0); 105 | animation: titleArrowHead 0.4s steps(4); 106 | } 107 | @keyframes titleArrowHead { 108 | from { 109 | transform: translateX(-100%); 110 | clip-path: inset(0 0 0 100%); 111 | } 112 | } 113 | 114 | .intro { 115 | margin-left: auto; 116 | margin-right: auto; 117 | } 118 | 119 | .heading-icon { 120 | margin: 0 1rem; 121 | float: right; 122 | text-shadow: 0 0 1px currentColor, 0 -1px 0 currentColor, 0 1px 0 currentColor, 123 | -1px 0 0 currentColor, 1px 0 0 currentColor; 124 | } 125 | 126 | .footer { 127 | font-size: 2rem; 128 | display: grid; 129 | grid-template-columns: minmax(0, 1fr) min-content minmax(0, 1fr); 130 | align-items: center; 131 | gap: 1.5ch; 132 | white-space: nowrap; 133 | overflow: hidden; 134 | } 135 | .footer > .support { 136 | display: flex; 137 | justify-content: end; 138 | opacity: 0.3; 139 | font-size: 55%; 140 | visibility: hidden; 141 | } 142 | .footer > .support ~ .support { 143 | justify-content: start; 144 | } 145 | .footer > h1 { 146 | margin: 0; 147 | display: inline-block; 148 | } 149 | .footer .title-decor { 150 | transition: opacity 0.2s, color 0.2s; 151 | } 152 | @media (min-width: 60rem) { 153 | .footer > .support { 154 | visibility: visible; 155 | } 156 | .footer.idle .title-decor { 157 | opacity: 0.3; 158 | color: inherit; 159 | } 160 | } 161 | .footer .support.entering { 162 | animation: heroSupportEntering 0.3s both ease-out; 163 | } 164 | .footer .support.exiting { 165 | animation: heroSupportExiting 0.3s both ease-in; 166 | } 167 | @keyframes heroSupportEntering { 168 | from { 169 | transform: translateX(-1ch); 170 | opacity: 0; 171 | } 172 | } 173 | @keyframes heroSupportExiting { 174 | to { 175 | opacity: 0; 176 | } 177 | } 178 | 179 | .subtitle { 180 | display: block; 181 | text-align: center; 182 | } 183 | 184 | .panel { 185 | border: none; 186 | border-bottom: solid 1px #0002; 187 | background: #f8fcff; 188 | border-radius: 0.5rem; 189 | } 190 | 191 | .code { 192 | padding: 0 0.25rem; 193 | background: #2e343f; 194 | color: #ddd; 195 | border-radius: 0.25rem; 196 | } 197 | .code.wide { 198 | white-space: pre; 199 | overflow: auto; 200 | max-width: unset; 201 | } 202 | pre.code { 203 | padding: 0.5rem; 204 | white-space: pre-wrap; 205 | word-break: break-all; 206 | border-radius: 0.5rem; 207 | } 208 | pre.code:has(code) { 209 | padding: 0; 210 | overflow: auto; 211 | } 212 | pre.code code { 213 | display: block; 214 | padding: 0.5rem !important; 215 | background: none; 216 | } 217 | .code i, 218 | .code-term { 219 | font-style: normal; 220 | color: #fd0; 221 | } 222 | .code b, 223 | .code-attention { 224 | font-weight: normal; 225 | color: #6ef; 226 | } 227 | .code-comment { 228 | color: #ddd; 229 | opacity: 0.6; 230 | } 231 | .code-target { 232 | color: #f86; 233 | } 234 | .code-template { 235 | color: #cfc; 236 | } 237 | .code-token { 238 | color: #acf; 239 | } 240 | 241 | .demos-section > [role="tablist"] > [role="tab"] { 242 | display: inline-block; 243 | padding: 0.5rem 1rem 0.5rem; 244 | border-bottom: solid 1px #0002; 245 | background: #f8fcff; 246 | border-radius: 0.5rem; 247 | } 248 | .demos-empty { 249 | grid-column: 1 / -1; 250 | grid-row: 1 / -1; 251 | display: flex; 252 | justify-content: center; 253 | align-items: center; 254 | font-size: 1.1rem; 255 | } 256 | 257 | #demos-tab-panel { 258 | display: grid; 259 | grid-auto-flow: column; 260 | grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); 261 | grid-template-rows: max-content 1fr; 262 | align-content: center; 263 | gap: 0.25rem 1rem; 264 | margin-top: 0.5rem; 265 | padding: 1rem; 266 | min-height: 25rem; 267 | } 268 | @media (max-width: 80rem) { 269 | #demos-tab-panel { 270 | grid-template-columns: minmax(0, 1fr); 271 | grid-template-rows: max-content max-content max-content max-content; 272 | } 273 | } 274 | #demos-tab-panel .code { 275 | margin: 0; 276 | white-space: pre; 277 | overflow: auto; 278 | max-width: none; 279 | } 280 | 281 | .demo-header { 282 | border: none; 283 | margin: 0; 284 | padding: 0; 285 | font-size: inherit; 286 | font-weight: bold; 287 | } 288 | .demo-multi-code { 289 | display: flex; 290 | flex-direction: column; 291 | align-items: stretch; 292 | gap: 0.5rem; 293 | } 294 | .demo-result { 295 | min-height: 15rem; 296 | background: white; 297 | border: inset 2px #dde; 298 | border-radius: 3px; 299 | color: black; 300 | overflow: auto; 301 | } 302 | 303 | .highlights-section { 304 | display: flex; 305 | gap: 1rem; 306 | } 307 | .highlights-section > div { 308 | flex: 1 0; 309 | } 310 | @media (max-width: 60rem) { 311 | .highlights-section { 312 | flex-direction: column; 313 | } 314 | } 315 | 316 | .mission { 317 | padding: 1rem; 318 | } 319 | 320 | @supports (background-clip: text) { 321 | pre.code code.rainbow-mask { 322 | display: inline-block; 323 | background-size: 215% 100%; 324 | background-clip: text; 325 | color: #fffd; 326 | animation: rainbow-mask 4s linear infinite; 327 | } 328 | } 329 | 330 | @keyframes rainbow-mask { 331 | from { 332 | background-image: linear-gradient(135deg in hsl longer hue, red, red 50%); 333 | } 334 | to { 335 | background-image: linear-gradient(135deg in hsl longer hue, red, red 50%); 336 | background-position: 100% 0; 337 | } 338 | } 339 | 340 | .examples-photo-grid { 341 | display: grid; 342 | grid-auto-flow: column; 343 | grid-template-columns: 2fr 3fr; 344 | grid-template-rows: repeat(2, min-content); 345 | gap: 1rem; 346 | } 347 | .examples-photo-grid img { 348 | width: 100%; 349 | object-position: top; 350 | object-fit: cover; 351 | border: solid 1px #0001; 352 | border-bottom-color: #0002; 353 | } 354 | .calendar-screenshot { 355 | grid-row: span 2; 356 | } 357 | @media (max-width: 80rem) { 358 | .examples-photo-grid { 359 | grid-template-columns: minmax(0, 32rem); 360 | grid-template-rows: repeat(4, min-content); 361 | } 362 | } -------------------------------------------------------------------------------- /docs/todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/docs/todo.png -------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/cat.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Felis est animalis elegantissimum et misteriosum. Felis est quadrupes, sed parvus et gracilis. Habent corpora flexilia et caudam longam. Oculi eorum sunt acuti et vigilantes, quae adiuvant eos in venatione. Felium natura est curiosa et independens, saepe explorans loca nova et exspectans tempus opportunitatis ad venandum. Multae sunt species felium, ab domesticis ad agrestes, et varietas colorum eorum est mirabilis. In multis culturis, felis est considerata animal domesticum, adfertque suavitatem et companiam in domum hominum. 4 |
-------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/dog.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Canis est animalis fidelis et amicus hominis. Canes sunt quadrupedes et pertingunt in multas formas et magnitudines. Exemplum, lupus est genus canis, sed domesticus canis est proximus amicus hominis. Canis habet olfactum acutum et auditum bonum, quod facit eum aptum ad custodiendum et adiuvandum hominem. Multa genera canum existunt, ab parvis et mollibus usque ad magnos et fortes. In multis culturis, canis est consideratus non solum animal domesticum sed etiam amicum fidelissimum. 4 |
-------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/horse.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Equus est animal nobilissimum et robustum. Equi sunt quadrupedes, magni et fortes. Habent crura longa et caudam fluam. Equi sunt celeres et agiles, idonei ad currum et vecturam. Oculi eorum sunt magni et vivaces, et auribus acutis auditum excellentem habent. Equi fuerunt auro pugnandi et trahendi plaustra ab antiquis temporibus, et usque ad hodiernum diem, sunt valde utiles in agris, in cursibus et in equestri exercitatione. Equi sunt etiam amici fideles hominum, qui saepe sunt asserunt sicut coniuncti socii in multis laboribus et gaudiis. 4 |
-------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

tabs

5 | 6 |

7 | tabs but with clean attribute values like
8 | <a href="dog.html" target="#my-tab-panel">. 9 |

10 | 11 |
12 | Dog 13 | Cat 14 | Horse 15 |
16 | 17 |
18 | 19 | 35 | -------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fff4ff; 6 | color: #313; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | .tab { 15 | display: inline-block; 16 | margin-inline-end: 0.25rem; 17 | padding: 0.5rem 1rem; 18 | border: solid 1px #313; 19 | border-bottom: none; 20 | border-top-left-radius: 0.5rem; 21 | border-top-right-radius: 0.5rem; 22 | } 23 | 24 | [role="tabpanel"] { 25 | padding: 1rem; 26 | border: solid 1px currentColor; 27 | } -------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/worker.js: -------------------------------------------------------------------------------- 1 | class IFrameInjector { 2 | element(element) { 3 | element.after( 4 | ``, 5 | { html: true } 6 | ); 7 | } 8 | } 9 | 10 | export default { 11 | async fetch(req) { 12 | const url = new URL(req.url); 13 | url.host = "localhost:3000"; 14 | const res = await fetch(new Request(url, req)); 15 | // For every element with an ID, inject an htmz iframe 16 | return new HTMLRewriter().on('[id]', new IFrameInjector()).transform(res); 17 | } 18 | } -------------------------------------------------------------------------------- /examples/cf_clean_target_tabs/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf_clean_target_tabs" 2 | main = "worker.js" 3 | compatibility_date = "2024-02-18" 4 | 5 | -------------------------------------------------------------------------------- /examples/html_clean_target_tabs/cat.html: -------------------------------------------------------------------------------- 1 |
2 | Felis est animalis elegantissimum et misteriosum. Felis est quadrupes, sed parvus et gracilis. Habent corpora flexilia et caudam longam. Oculi eorum sunt acuti et vigilantes, quae adiuvant eos in venatione. Felium natura est curiosa et independens, saepe explorans loca nova et exspectans tempus opportunitatis ad venandum. Multae sunt species felium, ab domesticis ad agrestes, et varietas colorum eorum est mirabilis. In multis culturis, felis est considerata animal domesticum, adfertque suavitatem et companiam in domum hominum. 3 |
-------------------------------------------------------------------------------- /examples/html_clean_target_tabs/dog.html: -------------------------------------------------------------------------------- 1 |
2 | Canis est animalis fidelis et amicus hominis. Canes sunt quadrupedes et pertingunt in multas formas et magnitudines. Exemplum, lupus est genus canis, sed domesticus canis est proximus amicus hominis. Canis habet olfactum acutum et auditum bonum, quod facit eum aptum ad custodiendum et adiuvandum hominem. Multa genera canum existunt, ab parvis et mollibus usque ad magnos et fortes. In multis culturis, canis est consideratus non solum animal domesticum sed etiam amicum fidelissimum. 3 |
-------------------------------------------------------------------------------- /examples/html_clean_target_tabs/horse.html: -------------------------------------------------------------------------------- 1 |
2 | Equus est animal nobilissimum et robustum. Equi sunt quadrupedes, magni et fortes. Habent crura longa et caudam fluam. Equi sunt celeres et agiles, idonei ad currum et vecturam. Oculi eorum sunt magni et vivaces, et auribus acutis auditum excellentem habent. Equi fuerunt auro pugnandi et trahendi plaustra ab antiquis temporibus, et usque ad hodiernum diem, sunt valde utiles in agris, in cursibus et in equestri exercitatione. Equi sunt etiam amici fideles hominum, qui saepe sunt asserunt sicut coniuncti socii in multis laboribus et gaudiis. 3 |
-------------------------------------------------------------------------------- /examples/html_clean_target_tabs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

tabs

4 | 5 |

6 | tabs but with clean attribute values like
7 | <a href="dog.html" target="#my-tab-panel">. 8 |

9 | 10 |
11 | Dog 12 | Cat 13 | Horse 14 |
15 | 16 | 17 |
18 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /examples/html_clean_target_tabs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fff4ff; 6 | color: #313; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | .tab { 15 | display: inline-block; 16 | margin-inline-end: 0.25rem; 17 | padding: 0.5rem 1rem; 18 | border: solid 1px #313; 19 | border-bottom: none; 20 | border-top-left-radius: 0.5rem; 21 | border-top-right-radius: 0.5rem; 22 | } 23 | 24 | [role="tabpanel"] { 25 | padding: 1rem; 26 | border: solid 1px currentColor; 27 | } -------------------------------------------------------------------------------- /examples/html_tabs/cat.html: -------------------------------------------------------------------------------- 1 |
2 | Felis est animalis elegantissimum et misteriosum. Felis est quadrupes, sed parvus et gracilis. Habent corpora flexilia et caudam longam. Oculi eorum sunt acuti et vigilantes, quae adiuvant eos in venatione. Felium natura est curiosa et independens, saepe explorans loca nova et exspectans tempus opportunitatis ad venandum. Multae sunt species felium, ab domesticis ad agrestes, et varietas colorum eorum est mirabilis. In multis culturis, felis est considerata animal domesticum, adfertque suavitatem et companiam in domum hominum. 3 |
-------------------------------------------------------------------------------- /examples/html_tabs/dog.html: -------------------------------------------------------------------------------- 1 |
2 | Canis est animalis fidelis et amicus hominis. Canes sunt quadrupedes et pertingunt in multas formas et magnitudines. Exemplum, lupus est genus canis, sed domesticus canis est proximus amicus hominis. Canis habet olfactum acutum et auditum bonum, quod facit eum aptum ad custodiendum et adiuvandum hominem. Multa genera canum existunt, ab parvis et mollibus usque ad magnos et fortes. In multis culturis, canis est consideratus non solum animal domesticum sed etiam amicum fidelissimum. 3 |
-------------------------------------------------------------------------------- /examples/html_tabs/horse.html: -------------------------------------------------------------------------------- 1 |
2 | Equus est animal nobilissimum et robustum. Equi sunt quadrupedes, magni et fortes. Habent crura longa et caudam fluam. Equi sunt celeres et agiles, idonei ad currum et vecturam. Oculi eorum sunt magni et vivaces, et auribus acutis auditum excellentem habent. Equi fuerunt auro pugnandi et trahendi plaustra ab antiquis temporibus, et usque ad hodiernum diem, sunt valde utiles in agris, in cursibus et in equestri exercitatione. Equi sunt etiam amici fideles hominum, qui saepe sunt asserunt sicut coniuncti socii in multis laboribus et gaudiis. 3 |
-------------------------------------------------------------------------------- /examples/html_tabs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

tabs

4 | 5 |

This is the iframe implementation of a simple tabbed UI example.

6 | 7 |
8 | Dog 9 | Cat 10 | Horse 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/html_tabs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

tabs

5 | 6 |

This is a simple tabbed UI example. Click on any of the tabs below.

7 | 8 |

The structure is very similar to an iframe implementation, except now the container can have a dynamic size and the content uses the same context and styles as the main page.

9 | 10 |

Back navigation & history works. Because these are native HTML links (<a>) that work normally. No listeners, no JS, no hijacking of normal web browser web browsing.

11 | 12 |
13 | Dog 14 | Cat 15 | Horse 16 |
17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/html_tabs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fff4ff; 6 | color: #313; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | .tab { 15 | display: inline-block; 16 | margin-inline-end: 0.25rem; 17 | padding: 0.5rem 1rem; 18 | border: solid 1px #313; 19 | border-bottom: none; 20 | border-top-left-radius: 0.5rem; 21 | border-top-right-radius: 0.5rem; 22 | } 23 | 24 | [role="tabpanel"] { 25 | padding: 1rem; 26 | border: solid 1px currentColor; 27 | } -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 |

htmz examples

3 | 8 |

apps

9 | 14 |

extensions

15 | -------------------------------------------------------------------------------- /examples/js_simple_error_handling/cat1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/examples/js_simple_error_handling/cat1.jpg -------------------------------------------------------------------------------- /examples/js_simple_error_handling/cat2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/examples/js_simple_error_handling/cat2.jpg -------------------------------------------------------------------------------- /examples/js_simple_error_handling/cat3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kalabasa/htmz/eec36a7245d60b3514c74d274d59a0618c5e2141/examples/js_simple_error_handling/cat3.jpg -------------------------------------------------------------------------------- /examples/js_simple_error_handling/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

simple error handling

5 | 6 |

7 | The default behaviour for htmz is to put any response, including error pages, 8 | into the target. 9 |

10 |

11 | This example employs an htmz extension that intercepts and handles error 12 | responses generically. 13 |

14 |

15 | Start slideshow! 16 |

17 | 18 |
19 | 20 | 21 | 45 | 46 | -------------------------------------------------------------------------------- /examples/js_simple_error_handling/slide1.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Prev 4 | Next ✅ 5 |
-------------------------------------------------------------------------------- /examples/js_simple_error_handling/slide2.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Prev ✅ 4 | Next ✅ 5 |
-------------------------------------------------------------------------------- /examples/js_simple_error_handling/slide3.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Prev ✅ 4 | Next ❌ 5 |
-------------------------------------------------------------------------------- /examples/js_simple_error_handling/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fff4ff; 6 | color: #313; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | img { 15 | width: 256px; 16 | height: 256px; 17 | object-fit: cover; 18 | display: block; 19 | } 20 | 21 | #slide a { 22 | display: inline-block; 23 | width: 128px; 24 | } -------------------------------------------------------------------------------- /examples/node_chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

no ajax no fetch chat

4 | 5 |

6 | Chat application without AJAX or fetch. It uses long polling to get real-time 7 | updates. 8 |

9 | 10 |

Open this page on a new browser window to test.

11 | 12 |
13 |
14 |
15 |
16 | join chatroom 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 133 | -------------------------------------------------------------------------------- /examples/node_chat/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const listen = require('./server.js'); 3 | 4 | const pastMessages = []; 5 | const subscribers = new Set(); 6 | 7 | const { basename } = listen([ 8 | // serve index.html 9 | ['/node_chat/', (req, res) => { 10 | const indexFile = __dirname + '/index.html'; 11 | const stat = fs.statSync(indexFile); 12 | res.setHeader('Content-Length', stat.size); 13 | fs.createReadStream(indexFile).pipe(res); 14 | }], 15 | 16 | ['/node_chat/chat', (req, res) => { 17 | res.setHeader('Cache-Control', 'no-cache, must-revalidate'); 18 | const reqURL = new URL(req.url, basename); 19 | if (reqURL.searchParams.has("join")) { 20 | receivePastMessages(res); 21 | } else { 22 | receiveNextMessage(res); 23 | } 24 | broadcastNewMessage(req); 25 | }], 26 | ]); 27 | 28 | function receivePastMessages(res) { 29 | res.end(html` 30 | 31 | ${pastMessages.join('\n')} 32 |
33 | `); 34 | } 35 | 36 | function receiveNextMessage(res) { 37 | const onMessage = (content) => { 38 | res.end(html` 39 | 40 | ${content} 41 |
42 | `); 43 | subscribers.delete(onMessage); 44 | }; 45 | subscribers.add(onMessage); 46 | } 47 | 48 | function broadcastNewMessage(req) { 49 | const reqURL = new URL(req.url, basename); 50 | 51 | if (!reqURL.searchParams.has("text")) return; 52 | 53 | const author = reqURL.searchParams.get("author") ?? 'anon'; 54 | const text = reqURL.searchParams.get("text"); 55 | const message = html` 56 |
57 | ${sanitize(author)} 58 | ${sanitize(text)} 59 |
60 | `; 61 | 62 | pastMessages.push(message); 63 | pastMessages.splice(0, pastMessages.length - 20); 64 | 65 | for (const onMessage of subscribers) { 66 | onMessage(message); 67 | } 68 | } 69 | 70 | function html(strings, ...values) { 71 | return String.raw({ raw: strings }, ...values); 72 | } 73 | 74 | function sanitize(value) { 75 | return value.toString() 76 | .replace(//g, '>'); 78 | } -------------------------------------------------------------------------------- /examples/node_chat/server.js: -------------------------------------------------------------------------------- 1 | const http = require('node:http'); 2 | 3 | const port = 4000; 4 | const basename = `http://localhost:${port}/`; 5 | 6 | function listen(routes) { 7 | const server = http.createServer(async (req, res) => { 8 | if (req.method !== 'GET') { 9 | res.statusCode = 405; 10 | res.end('use GET'); 11 | } 12 | 13 | for (const [pathname, cb] of routes) { 14 | const reqURL = new URL(req.url, basename); 15 | if (reqURL.pathname === pathname) { 16 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 17 | res.statusCode = 200; 18 | cb(req, res); 19 | return; 20 | } 21 | } 22 | 23 | res.statusCode = 404; 24 | res.end(); 25 | }); 26 | 27 | server.listen(port, () => { 28 | console.log(`node_chat server running at ${basename}`); 29 | }); 30 | 31 | return { basename }; 32 | } 33 | 34 | module.exports = listen; -------------------------------------------------------------------------------- /examples/php_calendar/create_event.php: -------------------------------------------------------------------------------- 1 | $date_str] = consume_props(); 5 | 6 | $create_slot_id = "create-slot-{$date_str}"; 7 | 8 | render('event.php', [ 9 | 'date_str' => $date_str, 10 | 'action' => 'create', 11 | ]); 12 | ?> 13 | 14 |
-------------------------------------------------------------------------------- /examples/php_calendar/date.php: -------------------------------------------------------------------------------- 1 | $date_str, // YYYY-MM-DD 6 | 'highlighted' => $highlighted, 7 | ] = consume_props() + [ 8 | 'highlighted' => false, 9 | ]; 10 | 11 | $date = date_parse($date_str); 12 | $create_slot_id = "create-slot-{$date_str}"; 13 | 14 | $events = array_filter( 15 | load_events($date['year'], $date['month']), 16 | fn($event) => $event['date_str'] === $date_str 17 | ); 18 | ?> 19 | 20 |
24 |
25 | 26 | 39 | 40 |
45 | 46 | 47 |
48 |
-------------------------------------------------------------------------------- /examples/php_calendar/delete_event.php: -------------------------------------------------------------------------------- 1 | $id] = consume_props(); 6 | 7 | delete_event($id); -------------------------------------------------------------------------------- /examples/php_calendar/event.php: -------------------------------------------------------------------------------- 1 | $id, 8 | 'date_str' => $date_str, 9 | 'content' => $content, 10 | 'color' => $color, 11 | 'action' => $action, // 'open' | 'close' | 'create' | 'save' 12 | ] = consume_props() + [ 13 | 'id' => uniqid(), 14 | 'content' => '', 15 | 'color' => '#ffcc00', 16 | 'action' => 'close', 17 | ]; 18 | 19 | $html_id = "event-{$id}"; 20 | $placeholder = $content ? $content : 'new entry'; 21 | $fg_color = get_fg_color($color); 22 | 23 | if ($action === 'save') { 24 | save_event([ 25 | 'id' => $id, 26 | 'date_str' => $date_str, 27 | 'content' => $content, 28 | 'color' => $color, 29 | ]); 30 | } 31 | ?> 32 | 33 |
  • 37 |
    41 | 42 | 43 | 44 | 45 | 46 | 54 |
    55 | 56 | 57 | 58 |
    62 | 63 | 64 |
    65 | 74 | 77 |
    78 |
    79 | 80 | 81 | 82 | 90 | 91 |
    92 |
    93 |
    94 | 95 |
  • -------------------------------------------------------------------------------- /examples/php_calendar/index.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |

    my calendar

    9 | 10 |

    This calendar is the most complex example.

    11 | 12 |

    Because switching months would replace the current month’s HTML with the new month’s HTML, there was a need to store application state beyond the confines of HTML. Thus, application data is persisted in cookies.

    13 | 14 |

    This application was implemented using a componentized approach. See util/props.php for the API.

    15 | 16 | $cur_date['year'], 21 | 'month' => $cur_date['mon'], 22 | ]); 23 | 24 | include '../../htmz.html'; -------------------------------------------------------------------------------- /examples/php_calendar/month.php: -------------------------------------------------------------------------------- 1 | $year, 7 | 'month' => $month 8 | ] = consume_props(); 9 | 10 | $year = intval2($year); 11 | $month = intval2($month); 12 | $day_count = days_in_month($month, $year); 13 | 14 | $month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 15 | $wday_names = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; 16 | 17 | $next_month = (($month + 1) - 1) % 12 + 1; 18 | $prev_month = (($month + 11) - 1) % 12 + 1; 19 | $next_month_year = $year + ($month === 12 ? 1 : 0); 20 | $prev_month_year = $year + ($month === 1 ? -1 : 0); 21 | 22 | $cur_date = getdate(); 23 | $month_start_date = getdate(strtotime("{$year}-{$month}-1")); 24 | $month_start_wday = $month_start_date['wday']; 25 | 26 | $month_name = $month_names[$month - 1]; 27 | $prev_month_name = $month_names[$prev_month - 1]; 28 | $next_month_name = $month_names[$next_month - 1]; 29 | 30 | $is_weekend = function($date) use($month_start_wday) { 31 | $wday = ($date + $month_start_wday) % 7; 32 | return $wday === 0 || $wday === 1; 33 | }; 34 | 35 | $is_past = function($date) use($cur_date, $year, $month) { 36 | $cur_year = $cur_date['year']; 37 | $cur_month = $cur_date['mon']; 38 | return $year < $cur_year || ($year === $cur_year && $month < $cur_month || $month === $cur_month && $date < $cur_date['mday']); 39 | }; 40 | 41 | $is_today = function($date) use($cur_date, $year, $month) { 42 | return $year === $cur_date['year'] && $month === $cur_date['mon'] && $date === $cur_date['mday']; 43 | }; 44 | 45 | function days_in_month($month, $year) { 46 | return $month == 2 ? ($year % 4 ? 28 : ($year % 100 ? 29 : ($year % 400 ? 28 : 29))) : (($month - 1) % 7 % 2 ? 30 : 31); 47 | } 48 | 49 | function intval2($str) { 50 | $val = intval($str); 51 | return $val === 0 ? null : $val; 52 | } 53 | ?> 54 | 55 |
    56 |
    57 |

    58 | 61 | ← 62 | 63 | 66 | → 67 | 68 |
    69 | 70 |
      71 | 72 |
    1. 73 | 74 | 75 | 0): ?> 76 |
      77 | 78 | 79 | 80 |
    2. 85 | "{$year}-{$month}-{$i}" 88 | ]); 89 | ?> 90 |
    3. 91 | 92 |
    93 |
    -------------------------------------------------------------------------------- /examples/php_calendar/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, 3 | segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, 4 | sans-serif; 5 | margin: 10vh auto; 6 | max-width: 120ch; 7 | background: #fffff4; 8 | color: #140; 9 | } 10 | 11 | h1 { 12 | font-size: 1.2rem; 13 | font-weight: bold; 14 | } 15 | 16 | input, 17 | button { 18 | border: none; 19 | color: inherit; 20 | font: inherit; 21 | } 22 | input { 23 | background: none; 24 | } 25 | input:focus-visible { 26 | outline: solid 1px #140; 27 | } 28 | button { 29 | background: #261; 30 | color: white; 31 | font-weight: bold; 32 | border-radius: 0.25rem; 33 | } 34 | button:hover, 35 | button:focus-visible { 36 | background: #483; 37 | } 38 | 39 | .month-control { 40 | display: flex; 41 | } 42 | .month-control h2 { 43 | font-size: 1rem; 44 | font-weight: bold; 45 | margin: 0; 46 | margin-inline-end: auto; 47 | } 48 | .month-control a { 49 | flex: 0 1 6ch; 50 | text-align: end; 51 | font-weight: bold; 52 | color: inherit; 53 | } 54 | 55 | .month-grid { 56 | list-style: none; 57 | padding: 0; 58 | display: grid; 59 | grid-template-columns: repeat(7, 1fr); 60 | } 61 | 62 | .wday-name { 63 | opacity: 0.6; 64 | text-align: center; 65 | padding: 0.5rem 0; 66 | } 67 | 68 | .day { 69 | border-top: solid 1px currentColor; 70 | min-width: 0; 71 | min-height: 8rem; 72 | } 73 | .day-weekend { 74 | color: #c88; 75 | } 76 | .day-past { 77 | color: #2423; 78 | } 79 | 80 | .date { 81 | height: 100%; 82 | display: flex; 83 | flex-direction: column; 84 | } 85 | .date ul { 86 | padding: 0; 87 | list-style: none; 88 | } 89 | 90 | .date-num { 91 | aspect-ratio: 1; 92 | border-radius: 100rem; 93 | background: #fffff4; 94 | position: absolute; 95 | z-index: 1; 96 | } 97 | .day-today .date-num { 98 | padding: 0 0.125em; 99 | background: #220; 100 | color: white; 101 | } 102 | 103 | .create-event-form { 104 | flex: 1 1 auto; 105 | margin: 0; 106 | } 107 | .create-event { 108 | background: none; 109 | color: transparent; 110 | width: 100%; 111 | height: 100%; 112 | cursor: crosshair; 113 | } 114 | .create-event:hover, 115 | .create-event:focus-visible { 116 | background: #efe4; 117 | } 118 | .create-event:focus-visible { 119 | color: #140; 120 | } 121 | 122 | .event { 123 | margin: 0.25rem 0.5rem; 124 | } 125 | .event:first-child { 126 | margin-top: 0.5rem; 127 | } 128 | 129 | .event > form { 130 | display: flex; 131 | margin: 0; 132 | } 133 | 134 | .event-button { 135 | position: relative; 136 | flex: 1 1 auto; 137 | padding: 0.25rem; 138 | background: white; 139 | color: #000b; 140 | border-radius: 0.25rem; 141 | text-wrap: nowrap; 142 | text-overflow: ellipsis; 143 | overflow: hidden; 144 | text-align: start; 145 | font-size: 0.8rem; 146 | font-weight: bold; 147 | text-decoration: none; 148 | } 149 | .event-button::after { 150 | content: ""; 151 | position: absolute; 152 | inset: 0; 153 | } 154 | .event-button:hover::after, 155 | .event-button:focus-visible::after { 156 | background: #fff4; 157 | } 158 | .day-past .event:not(:focus-within):not(:has(dialog[open])) .event-button { 159 | opacity: 0.4; 160 | } 161 | 162 | .event dialog { 163 | inset: unset; 164 | margin-top: 0.3rem; 165 | padding: 0.75rem; 166 | background: white; 167 | border: solid 1px #ceb; 168 | box-shadow: 0 0.25rem 0.5rem #0301, 0 0rem 2rem #0301; 169 | z-index: 9999; 170 | } 171 | .event dialog::after { 172 | content: ''; 173 | position: absolute; 174 | left: 0.4rem; 175 | top: calc(-0.3rem - 2px); 176 | width: 0.6rem; 177 | height: 0.6rem; 178 | transform: rotate(45deg); 179 | background: inherit; 180 | border: inherit; 181 | border-right: none; 182 | border-bottom: none; 183 | } 184 | 185 | .event dialog form { 186 | margin: 0; 187 | display: flex; 188 | flex-direction: column; 189 | gap: 0.5rem; 190 | } 191 | .event-dialog-row { 192 | display: flex; 193 | gap: 0.5rem; 194 | } 195 | 196 | .event input[type="text"] { 197 | border-bottom: solid 1px currentColor; 198 | max-width: 15ch; 199 | } 200 | 201 | .delete-button { 202 | background: #820; 203 | } 204 | .delete-button:hover, 205 | .delete-button:focus-visible { 206 | background: #a42; 207 | } 208 | -------------------------------------------------------------------------------- /examples/php_calendar/util/color.php: -------------------------------------------------------------------------------- 1 | $id, 'text' => $text] = consume_props(); 11 | // 12 | function consume_props() { 13 | $props = $_REQUEST; 14 | $_REQUEST = []; 15 | return $props; 16 | } 17 | 18 | // Render a component with props 19 | // 20 | // render('item.php', ['id' => 0, 'text' => 'foo']); 21 | // 22 | function render($filename, $props = []) { 23 | set_props($props); 24 | include $filename; 25 | } 26 | 27 | // Before invoking a component, set their props. 28 | // Unlike `render`, this executes the component in the global scope. 29 | // 30 | // set_props(['id' => 0, 'text' => 'foo']); 31 | // include('item.php'); 32 | // 33 | function set_props($props) { 34 | $_REQUEST = $props; 35 | } 36 | -------------------------------------------------------------------------------- /examples/php_dialog/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

    dialog

    5 | 6 |

    Dialog picker using native HTML (<dialog>).

    7 | 8 |

    See picker.php. It represents a stateful UI component, but no state is stored server-side! The HTML is the state.

    9 | 10 |

    One limitation of dialogs is that you can’t have a modal dialog in pure HTML. You would have to do it the normal way (that is, client-side scripting for highly-interactive elements).

    11 | 12 |

    Back navigation & history works, surprisingly. You can ‘time travel’ through application state natively using your browser’s history. But for real, I feel like this would only work for simple UIs (like a single target).

    13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/php_dialog/picker.php: -------------------------------------------------------------------------------- 1 | $action, 4 | 'class' => $class, 5 | ] = $_REQUEST; 6 | ?> 7 | 8 | 9 | 10 |
    14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 |
    Select a class
    26 | 27 | 28 | 29 |
    30 | 31 |
    32 |
    -------------------------------------------------------------------------------- /examples/php_dialog/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fffff4; 6 | color: #331; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | 15 | button { 16 | border: none; 17 | background: #651; 18 | color: white; 19 | font: inherit; 20 | font-weight: bold; 21 | border-radius: 0.25rem; 22 | } 23 | button:hover, button:focus-visible { 24 | background: #763; 25 | } 26 | 27 | dialog { 28 | z-index: 1; 29 | } 30 | 31 | form { 32 | display: inline-block; 33 | margin: 0; 34 | } -------------------------------------------------------------------------------- /examples/php_new_tab_detection/content.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | This is the content. 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/php_new_tab_detection/footer.php: -------------------------------------------------------------------------------- 1 |

    2 | This uses the new header Sec-Fetch-Dest which lets the server know the request's destination. 3 |

    -------------------------------------------------------------------------------- /examples/php_new_tab_detection/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    Load content. Try opening the 'Load content' link in a new tab

    4 | 5 |
    6 |
    No content
    7 |
    8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/php_new_tab_detection/prelude.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

    new tab detection

    5 | 6 |

    Detect when a URL is loaded in the htmz iframe or in a new tab and render either a fragment or a full page appropriately

    7 | -------------------------------------------------------------------------------- /examples/php_new_tab_detection/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fffff4; 6 | color: #331; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | .content-container { 15 | border: solid 1px currentColor; 16 | padding: 1rem; 17 | } -------------------------------------------------------------------------------- /examples/php_todo/clear.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/php_todo/create.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | -------------------------------------------------------------------------------- /examples/php_todo/edit.php: -------------------------------------------------------------------------------- 1 | 5 | 6 |
  • 10 |
    14 | 15 | 22 | 23 | 29 |
    30 |
  • -------------------------------------------------------------------------------- /examples/php_todo/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

    my todo list

    5 | 6 |
    11 | 12 | 13 |
    14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /examples/php_todo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #fffff4; 6 | color: #410; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | 14 | input, button { 15 | border: none; 16 | color: inherit; 17 | font: inherit; 18 | } 19 | input { 20 | background: none; 21 | } 22 | input:focus-visible { 23 | outline: solid 1px #410; 24 | } 25 | button { 26 | background: #621; 27 | color: white; 28 | font-weight: bold; 29 | border-radius: 0.25rem; 30 | } 31 | button:hover, button:focus-visible { 32 | background: #843; 33 | } 34 | 35 | .create-form { 36 | display: flex; 37 | margin: 0; 38 | height: 2.5rem; 39 | border: solid 1px #ecb; 40 | background: #fcfcf4; 41 | } 42 | .create-form > input { 43 | flex: 1 1 auto; 44 | padding: 0 0.5rem; 45 | } 46 | .create-form > button { 47 | flex: 0 1 auto; 48 | margin: 0.25rem; 49 | } 50 | 51 | ul { 52 | list-style: none; 53 | margin: 0; 54 | padding-inline: 0; 55 | } 56 | .todo-item { 57 | border-bottom: solid 1px #ecb; 58 | margin: 0; 59 | } 60 | 61 | .todo-item > form { 62 | display: flex; 63 | margin: 0; 64 | height: 2.5rem; 65 | align-items: stretch; 66 | } 67 | 68 | .todo-item-content { 69 | order: 2; 70 | flex: 1 1 auto; 71 | } 72 | 73 | .todo-item-save { 74 | order: 3; 75 | margin: 0.25rem; 76 | opacity: 0; 77 | } 78 | .todo-item-save:focus, 79 | .todo-item-content:focus ~ .todo-item-save { 80 | opacity: 1; 81 | } 82 | 83 | .todo-item-clear { 84 | order: 1; 85 | margin: 0.5rem; 86 | aspect-ratio: 1; 87 | background: none; 88 | color: transparent; 89 | border: solid 1px #ecb; 90 | border-radius: 500%; 91 | display: flex; 92 | justify-content: center; 93 | align-items: center; 94 | font-size: 1.5rem; 95 | text-decoration: none; 96 | } 97 | .todo-item-clear:hover, .todo-item-clear:focus-visible { 98 | background: none; 99 | color: inherit; 100 | } 101 | .todo-item:has(.todo-item-clear:hover, .todo-item-clear:focus-visible) .todo-item-content { 102 | opacity: 0.4; 103 | text-decoration: line-through; 104 | } -------------------------------------------------------------------------------- /examples/run_servers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | php -S localhost:3000 -t . & 3 | node node_chat & 4 | wrangler dev --config cf_clean_target_tabs/wrangler.toml --port 5000 & 5 | wait -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | margin: 20vh auto; 4 | max-width: 60ch; 5 | background: #f4ffff; 6 | color: #014; 7 | } 8 | 9 | h1 { 10 | font-size: 1.2rem; 11 | font-weight: bold; 12 | } 13 | h2 { 14 | font-size: 1.05rem; 15 | font-weight: bold; 16 | } -------------------------------------------------------------------------------- /glitch.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "cd examples && ./run_servers.sh" 3 | } -------------------------------------------------------------------------------- /htmz.dev.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /htmz.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /npx/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /npx/README.md: -------------------------------------------------------------------------------- 1 | This package is a joke for npm enjoyers. Just copy the snippet. 2 | 3 | I mean... it works, if you really want to automate copying the snippet. -------------------------------------------------------------------------------- /npx/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs-extra"; 3 | import yargs from "yargs/yargs"; 4 | import { hideBin } from "yargs/helpers"; 5 | import { RewritingStream } from 'parse5-html-rewriting-stream'; 6 | import ss from "stream-string"; 7 | 8 | const snippetHolder = [``]; 9 | 10 | const args = yargs(hideBin(process.argv)) 11 | .scriptName("npx htmzify") 12 | .example("npx htmzify src/index.html") 13 | .command("$0 ", "Installs htmz into an HTML file", (yargs) => yargs 14 | .positional("path", { 15 | type: "string", 16 | desc: "Path to the HTML file" 17 | }) 18 | .option("nobak", { 19 | type: "boolean", 20 | desc: "Do not make a backup copy" 21 | }) 22 | ) 23 | .parse() 24 | 25 | const filePath = args.path; 26 | 27 | let cancel = false; 28 | let prevIndent = ""; 29 | let prevPrevIndent = ""; 30 | const rewriter = new RewritingStream(); 31 | 32 | rewriter.on("startTag", (startTag) => { 33 | if (startTag.tagName === "iframe" && startTag.attrs.find(attr => attr.name === "name" && attr.value === "htmz")) { 34 | cancel = true; 35 | rewriter.stop(); 36 | } 37 | rewriter.emitStartTag(startTag); 38 | }); 39 | 40 | rewriter.on("text", (_, text) => { 41 | const indents = [...text.matchAll(/[\n\r](\s*)/g)]; 42 | if (indents.length > 0) { 43 | prevPrevIndent = prevIndent; 44 | prevIndent = indents[indents.length - 1][1]; 45 | } 46 | rewriter.emitRaw(text); 47 | }); 48 | 49 | rewriter.on("endTag", (endTag) => { 50 | if (snippetHolder.length && (endTag.tagName === "html" || endTag.tagName === "body")) { 51 | rewriter.emitRaw(formatSnippet(snippetHolder.pop())); 52 | } 53 | rewriter.emitEndTag(endTag); 54 | }); 55 | 56 | function formatSnippet(snippet) { 57 | return prevPrevIndent.slice(prevIndent.length) + snippet + "\n" + prevIndent; 58 | } 59 | 60 | const readStream = fs.createReadStream(filePath, { encoding: "utf-8" }); 61 | let transformed = await ss(readStream.pipe(rewriter)); 62 | 63 | if (cancel) process.exit(0); 64 | 65 | if (snippetHolder.length) { 66 | transformed += formatSnippet(snippetHolder.pop()); 67 | } 68 | 69 | if (!args.nobak) fs.moveSync(filePath, filePath + ".bak", { overwrite: true }); 70 | fs.writeFileSync(filePath, transformed); 71 | -------------------------------------------------------------------------------- /npx/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmz", 3 | "version": "1.0.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "htmz", 9 | "version": "1.0.2", 10 | "dependencies": { 11 | "fs-extra": "^11.2.0", 12 | "parse5-html-rewriting-stream": "^7.0.0", 13 | "stream-string": "^2.0.4", 14 | "yargs": "^17.7.2" 15 | }, 16 | "bin": { 17 | "htmz": "index.js", 18 | "htmzify": "index.js" 19 | } 20 | }, 21 | "node_modules/ansi-regex": { 22 | "version": "5.0.1", 23 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 24 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 25 | "engines": { 26 | "node": ">=8" 27 | } 28 | }, 29 | "node_modules/ansi-styles": { 30 | "version": "4.3.0", 31 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 32 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 33 | "dependencies": { 34 | "color-convert": "^2.0.1" 35 | }, 36 | "engines": { 37 | "node": ">=8" 38 | }, 39 | "funding": { 40 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 41 | } 42 | }, 43 | "node_modules/cliui": { 44 | "version": "8.0.1", 45 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 46 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 47 | "dependencies": { 48 | "string-width": "^4.2.0", 49 | "strip-ansi": "^6.0.1", 50 | "wrap-ansi": "^7.0.0" 51 | }, 52 | "engines": { 53 | "node": ">=12" 54 | } 55 | }, 56 | "node_modules/color-convert": { 57 | "version": "2.0.1", 58 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 59 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 60 | "dependencies": { 61 | "color-name": "~1.1.4" 62 | }, 63 | "engines": { 64 | "node": ">=7.0.0" 65 | } 66 | }, 67 | "node_modules/color-name": { 68 | "version": "1.1.4", 69 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 70 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 71 | }, 72 | "node_modules/emoji-regex": { 73 | "version": "8.0.0", 74 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 75 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 76 | }, 77 | "node_modules/entities": { 78 | "version": "4.5.0", 79 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 80 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 81 | "engines": { 82 | "node": ">=0.12" 83 | }, 84 | "funding": { 85 | "url": "https://github.com/fb55/entities?sponsor=1" 86 | } 87 | }, 88 | "node_modules/escalade": { 89 | "version": "3.1.2", 90 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 91 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 92 | "engines": { 93 | "node": ">=6" 94 | } 95 | }, 96 | "node_modules/fs-extra": { 97 | "version": "11.2.0", 98 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", 99 | "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", 100 | "dependencies": { 101 | "graceful-fs": "^4.2.0", 102 | "jsonfile": "^6.0.1", 103 | "universalify": "^2.0.0" 104 | }, 105 | "engines": { 106 | "node": ">=14.14" 107 | } 108 | }, 109 | "node_modules/get-caller-file": { 110 | "version": "2.0.5", 111 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 112 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 113 | "engines": { 114 | "node": "6.* || 8.* || >= 10.*" 115 | } 116 | }, 117 | "node_modules/graceful-fs": { 118 | "version": "4.2.11", 119 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 120 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 121 | }, 122 | "node_modules/is-fullwidth-code-point": { 123 | "version": "3.0.0", 124 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 125 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 126 | "engines": { 127 | "node": ">=8" 128 | } 129 | }, 130 | "node_modules/jsonfile": { 131 | "version": "6.1.0", 132 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 133 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 134 | "dependencies": { 135 | "universalify": "^2.0.0" 136 | }, 137 | "optionalDependencies": { 138 | "graceful-fs": "^4.1.6" 139 | } 140 | }, 141 | "node_modules/parse5": { 142 | "version": "7.1.2", 143 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 144 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 145 | "dependencies": { 146 | "entities": "^4.4.0" 147 | }, 148 | "funding": { 149 | "url": "https://github.com/inikulin/parse5?sponsor=1" 150 | } 151 | }, 152 | "node_modules/parse5-html-rewriting-stream": { 153 | "version": "7.0.0", 154 | "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", 155 | "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", 156 | "dependencies": { 157 | "entities": "^4.3.0", 158 | "parse5": "^7.0.0", 159 | "parse5-sax-parser": "^7.0.0" 160 | }, 161 | "funding": { 162 | "url": "https://github.com/inikulin/parse5?sponsor=1" 163 | } 164 | }, 165 | "node_modules/parse5-sax-parser": { 166 | "version": "7.0.0", 167 | "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", 168 | "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", 169 | "dependencies": { 170 | "parse5": "^7.0.0" 171 | }, 172 | "funding": { 173 | "url": "https://github.com/inikulin/parse5?sponsor=1" 174 | } 175 | }, 176 | "node_modules/require-directory": { 177 | "version": "2.1.1", 178 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 179 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 180 | "engines": { 181 | "node": ">=0.10.0" 182 | } 183 | }, 184 | "node_modules/stream-string": { 185 | "version": "2.0.4", 186 | "resolved": "https://registry.npmjs.org/stream-string/-/stream-string-2.0.4.tgz", 187 | "integrity": "sha512-5s9qfQmrgEMxBTmFgYQZI+5yhorrOjBUJGnkOXUXmxiNmtfhlOwFp+YNCf7Tsu+i2eHWYaDRVb9MvCPwSdBkqw==" 188 | }, 189 | "node_modules/string-width": { 190 | "version": "4.2.3", 191 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 192 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 193 | "dependencies": { 194 | "emoji-regex": "^8.0.0", 195 | "is-fullwidth-code-point": "^3.0.0", 196 | "strip-ansi": "^6.0.1" 197 | }, 198 | "engines": { 199 | "node": ">=8" 200 | } 201 | }, 202 | "node_modules/strip-ansi": { 203 | "version": "6.0.1", 204 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 205 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 206 | "dependencies": { 207 | "ansi-regex": "^5.0.1" 208 | }, 209 | "engines": { 210 | "node": ">=8" 211 | } 212 | }, 213 | "node_modules/universalify": { 214 | "version": "2.0.1", 215 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", 216 | "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", 217 | "engines": { 218 | "node": ">= 10.0.0" 219 | } 220 | }, 221 | "node_modules/wrap-ansi": { 222 | "version": "7.0.0", 223 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 224 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 225 | "dependencies": { 226 | "ansi-styles": "^4.0.0", 227 | "string-width": "^4.1.0", 228 | "strip-ansi": "^6.0.0" 229 | }, 230 | "engines": { 231 | "node": ">=10" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 235 | } 236 | }, 237 | "node_modules/y18n": { 238 | "version": "5.0.8", 239 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 240 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 241 | "engines": { 242 | "node": ">=10" 243 | } 244 | }, 245 | "node_modules/yargs": { 246 | "version": "17.7.2", 247 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 248 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 249 | "dependencies": { 250 | "cliui": "^8.0.1", 251 | "escalade": "^3.1.1", 252 | "get-caller-file": "^2.0.5", 253 | "require-directory": "^2.1.1", 254 | "string-width": "^4.2.3", 255 | "y18n": "^5.0.5", 256 | "yargs-parser": "^21.1.1" 257 | }, 258 | "engines": { 259 | "node": ">=12" 260 | } 261 | }, 262 | "node_modules/yargs-parser": { 263 | "version": "21.1.1", 264 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 265 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 266 | "engines": { 267 | "node": ">=12" 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /npx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmz", 3 | "version": "1.0.2", 4 | "type": "module", 5 | "bin": { 6 | "htmz": "index.js", 7 | "htmzify": "index.js" 8 | }, 9 | "dependencies": { 10 | "fs-extra": "^11.2.0", 11 | "parse5-html-rewriting-stream": "^7.0.0", 12 | "stream-string": "^2.0.4", 13 | "yargs": "^17.7.2" 14 | } 15 | } 16 | --------------------------------------------------------------------------------