├── changelog.md
├── css
├── main.css
└── reset.css
├── fonts
├── PlombSans-Bold.woff2
└── PlombSans-Regular.woff2
├── images
└── screenshot.png
├── index.html
├── js
├── AppState.js
├── Feature.js
├── Filter.js
├── Filters.js
├── Font.js
├── Fonts.js
├── Helpers.js
├── Layout.js
├── Line.js
├── Size.js
├── Words.js
├── app.js
├── components
│ ├── About.js
│ ├── CopyButton.js
│ ├── CopyButtonGlobal.js
│ ├── DarkModeButton.js
│ ├── DeleteButton.js
│ ├── DeleteButtonGlobal.js
│ ├── DropZone.js
│ ├── FeaturesMenu.js
│ ├── FilterSelect.js
│ ├── FilterSelectGlobal.js
│ ├── FontInput.js
│ ├── FontSelect.js
│ ├── FontSelectGlobal.js
│ ├── Footer.js
│ ├── Header.js
│ ├── IconSpinning.js
│ ├── Line.js
│ ├── NewLineButton.js
│ ├── OptionsMenu.js
│ ├── SVG.js
│ ├── SVGAnimation.js
│ ├── SizeInput.js
│ ├── SizeInputGlobal.js
│ ├── Specimen.js
│ ├── SplashScreen.js
│ ├── Tooltip.js
│ ├── UpdateButton.js
│ ├── UpdateButtonGlobal.js
│ └── WidthInput.js
├── harfbuzzjs
│ ├── hb.wasm
│ └── hbjs.js
├── miniotparser
│ └── MiniOTParser.js
├── vendor
│ └── mithril.min.js
└── wordgenerator
│ ├── WordGenerator.js
│ ├── WorkerPool.js
│ └── worker.js
├── license.md
├── readme.md
├── svg
├── font-files-animation.svg
├── logo.svg
└── stack-and-justify-animation.svg
└── words
├── dictionaries
├── catalan.json
├── czech.json
├── danish.json
├── dutch.json
├── english.json
├── finnish.json
├── french.json
├── german.json
├── hungarian.json
├── icelandic.json
├── italian.json
├── latin.json
├── norwegian.json
├── polish.json
├── slovak.json
└── spanish.json
└── wikipedia
├── ca_wikipedia.json
├── cs_wikipedia.json
├── da_wikipedia.json
├── de_wikipedia.json
├── en_wikipedia.json
├── es_wikipedia.json
├── fi_wikipedia.json
├── fr_wikipedia.json
├── hu_wikipedia.json
├── is_wikipedia.json
├── it_wikipedia.json
├── la_wikipedia.json
├── nl_wikipedia.json
├── no_wikipedia.json
├── pl_wikipedia.json
└── sk_wikipedia.json
/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.10
4 |
5 | ### Added
6 | - Add support for OpenType features
7 |
8 | ### Changed
9 | - Fonts are sorted using font metadatas (usWeightClass, usWidthClass) instead of style names
10 | - Fonts are grouped by family in the font inputs
11 | - Small design fixes in menus and inputs
12 | - Use Harfbuzz instead of Canvas API for measuring strings.
13 |
14 | ### Fixed
15 | - Selected options in the Options menu are now persistent even when not applied (also applies to the Features menu)
16 |
17 |
18 | ## 1.02
19 | - Add basic CSS to improve apparence on mobile devices
20 | - Improve SEO by adding meta description and OG tags
21 | - Fix a bug where the font select inputs were troncated
22 | - Fix the sorting of fonts with numbers as style names
23 |
24 |
25 | ## 1.01
26 | - Improve browser support
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | /* Typography */
2 |
3 | @font-face {
4 | font-family: "Plomb Sans";
5 | src: url("../fonts/PlombSans-Regular.woff2") format("woff2");
6 | font-weight: 400;
7 | }
8 |
9 | @font-face {
10 | font-family: "Plomb Sans";
11 | src: url("../fonts/PlombSans-Bold.woff2") format("woff2");
12 | font-weight: 700;
13 | }
14 |
15 | :root {
16 | font-family: Plomb Sans;
17 | font-size: 14px;
18 | line-height: 1.2;
19 | }
20 |
21 | .t-big {
22 | font-size: 28px;
23 | line-height: 1.3;
24 | }
25 |
26 | .bold {
27 | font-weight: 700;
28 | }
29 |
30 | @media screen and (max-width: 430px) {
31 | :root {
32 | font-size: 13px;
33 | }
34 |
35 | .t-big {
36 | font-size: 19.5px;
37 | }
38 | }
39 |
40 |
41 | /* Base */
42 |
43 | html {
44 | display: flex;
45 | min-height: 100vh;
46 | }
47 |
48 | body {
49 | padding: 2rem;
50 | margin: 0;
51 | box-sizing: border-box;
52 | background: var(--bg-color);
53 | color: var(--fg-color);
54 | display: flex;
55 | flex: 1;
56 | min-height: 100vh;
57 | transition: background 0.1s, color 0.2s;
58 |
59 | --bg-color: #FFF;
60 | --fg-color: #000;
61 | --grey-light: #F9F9F9;
62 | --grey: #A0A0A0;
63 | --green: #00b47c;
64 | --green-light: #00e682;
65 | }
66 |
67 | body.dark {
68 | --bg-color: #000;
69 | --fg-color: #FFF;
70 | --grey-light: #171717;
71 | --green: #00e682;
72 | --green-light: #00b47c;
73 | }
74 |
75 | input[disabled] {
76 | -webkit-text-fill-color: var(--fg-color);
77 | }
78 |
79 | #app {
80 | flex: 1;
81 | display: flex;
82 | flex-direction: column;
83 | }
84 |
85 | .main {
86 | position: relative;
87 | flex: 1
88 | }
89 |
90 | @media screen and (max-width: 430px) {
91 | body {
92 | max-width: 100%;
93 | padding: 1.5rem;
94 | }
95 |
96 | #app {
97 | max-width: 100%;
98 | }
99 | }
100 |
101 |
102 |
103 | /* Links */
104 |
105 | .disabled {
106 | opacity: 0.3;
107 | }
108 |
109 | .disabled:hover {
110 | cursor: default;
111 | color: inherit;
112 | }
113 |
114 | button {
115 | cursor: pointer;
116 | }
117 |
118 | a {
119 | border-bottom: 1px dotted;
120 | cursor: pointer;
121 | }
122 |
123 | a:hover,
124 | button:hover {
125 | color: var(--green);
126 | }
127 |
128 | a:active,
129 | button:active {
130 | color: var(--green-light);
131 | }
132 |
133 | .big-link::after {
134 | content: '↗';
135 | font-size: 1rem;
136 | margin-left: 0.15rem;
137 | vertical-align: text-top;
138 | }
139 |
140 |
141 | /* Header */
142 |
143 | .header {
144 | position: relative;
145 | display: grid;
146 | grid-column-gap: 2rem;
147 | grid-template-columns: 2fr 3fr 1fr 1fr 1fr;
148 | padding-bottom: 4rem;
149 | }
150 |
151 | .logo {
152 | display: flex;
153 | align-items: flex-start;
154 | font-weight: 700;
155 | user-select: none;
156 | -webkit-user-select: none;
157 | }
158 |
159 | .logo svg {
160 | position: relative;
161 | top: -20%;
162 | margin-right: 0.75rem;
163 | }
164 |
165 | .logo svg .fill {
166 | transition: 0.15s fill;
167 | }
168 |
169 | body.dark .logo svg .fill {
170 | fill: var(--fg-color);
171 | }
172 |
173 | .drop-message {
174 | user-select: none;
175 | -webkit-user-select: none;
176 | }
177 |
178 | .drop-btn {
179 | white-space: nowrap;
180 | }
181 |
182 | .header-btns {
183 | text-align: right;
184 | user-select: none;
185 | -webkit-user-select: none;
186 | white-space: nowrap;
187 | }
188 |
189 | .dark-mode-btn {
190 | position: relative;
191 | margin-right: 0.5rem;
192 | z-index: 1;
193 | }
194 |
195 | @media screen and (max-width: 430px) {
196 | .header {
197 | display: flex;
198 | flex-wrap: wrap;
199 | justify-content: space-between;
200 | padding-bottom: 0;
201 | }
202 |
203 | .logo {
204 | order: 0;
205 | margin-bottom: 0.5rem;
206 | }
207 |
208 | .header-btns {
209 | order: 1;
210 | }
211 |
212 | .options {
213 | display: none;
214 | }
215 |
216 | .features {
217 | display: none;
218 | }
219 |
220 | .drop-message {
221 | order: 2;
222 | }
223 | }
224 |
225 | /* Footer */
226 |
227 | .footer {
228 | display: flex;
229 | justify-content: space-between;
230 | margin-top: 4rem;
231 | }
232 |
233 | .footer .credit,
234 | .footer .github {
235 | user-select: none;
236 | -webkit-user-select: none;
237 | }
238 |
239 | @media screen and (max-width: 430px) {
240 | .footer {
241 | margin-top: 0;
242 | flex-wrap: wrap;
243 | }
244 |
245 | .footer .credit {
246 | margin-bottom: 0.75rem;
247 | }
248 | }
249 |
250 |
251 | /* Drop Zone */
252 |
253 | .drop-zone {
254 | position: fixed;
255 | top: 0;
256 | left: 0;
257 | right: 0;
258 | bottom: 0;
259 | z-index: 10;
260 | visibility: hidden;
261 | opacity: 0;
262 | background-color: var(--green);
263 | transition: visibility 175ms, opacity 175ms;
264 | }
265 |
266 | .drop-zone.active {
267 | opacity: 0.85;
268 | visibility: visible;
269 | }
270 |
271 |
272 | /* Splash Screen */
273 |
274 | .splash-screen {
275 | border-top: 1px solid;
276 | padding-top: 0.5rem;
277 | margin-top: 3.285rem;
278 | }
279 |
280 | .splash-screen-text {
281 | max-width: 34rem;
282 | }
283 |
284 | .splash-screen svg {
285 | margin-bottom: 0.5rem;
286 | }
287 |
288 | .splash-screen-notice {
289 | margin-top: 1rem;
290 | }
291 |
292 |
293 | /* Specimen */
294 |
295 | .specimen {
296 | display: grid;
297 | grid-column-gap: 2rem;
298 | grid-template-columns: [left] 1fr [middle] auto [right] 1fr;
299 | }
300 |
301 | .specimen-header {
302 | display: grid;
303 | grid-column: span 3;
304 | grid-template-columns: subgrid;
305 | border-bottom: 1px solid;
306 | }
307 |
308 | .specimen-body {
309 | display: grid;
310 | grid-column: span 3;
311 | grid-template-columns: subgrid;
312 | }
313 |
314 | .specimen-line {
315 | position: relative;
316 | display: grid;
317 | grid-column: span 3;
318 | grid-template-columns: subgrid;
319 | border-bottom: 1px dotted;
320 | cursor: grab;
321 | transition: background-color 0.2s;
322 | }
323 |
324 | .specimen-line:hover {
325 | background-color: var(--grey-light);
326 |
327 | }
328 |
329 | @supports not (grid-template-columns: subgrid) {
330 | .specimen-line,
331 | .specimen-header {
332 | grid-column-gap: 2rem;
333 | grid-template-columns: [left] 1fr [middle] auto [right] 1fr;
334 | }
335 | }
336 |
337 | .specimen-line.dragged * {
338 | visibility: hidden;
339 | }
340 |
341 | .specimen-line.drag-clone {
342 | position: fixed;
343 | background: var(--bg-color);
344 | pointer-events: none;
345 | z-index: 10;
346 | border-top: 1px dotted;
347 | bottom: 1px dotted;
348 | box-shadow: 1px 2px 2px rgba(0,0,0,0.25);
349 | }
350 |
351 | .line-left-col {
352 | grid-column: left;
353 | display: flex;
354 | align-items: flex-start;
355 | padding-top: 1rem;
356 | padding-bottom: 1rem;
357 | user-select: none;
358 | -webkit-user-select: none;
359 | }
360 |
361 | .line-middle-col {
362 | position: relative;
363 | grid-column: middle;
364 | display: flex;
365 | justify-content: center;
366 | }
367 |
368 | .specimen-line .text {
369 | align-self: center;
370 | cursor: text;
371 | transition: opacity 0.15s;
372 | }
373 |
374 | .specimen-line .text.visible {
375 | opacity: 1;
376 | }
377 |
378 | .specimen-line .text.hidden {
379 | opacity: 0;
380 | }
381 |
382 |
383 | .specimen-line .loading {
384 | position: absolute;
385 | top: 0;
386 | left: 0;
387 | width: 100%;
388 | height: 100%;
389 | display: flex;
390 | justify-content: center;
391 | align-items: center;
392 | color: #A0A0A0;
393 | /* background: var(--bg-color);*/
394 | transition: opacity 0.15s;
395 | pointer-events: none;
396 | user-select: none;
397 | -webkit-user-select: none;
398 | }
399 |
400 | .specimen-line .loading.hidden {
401 | opacity: 0;
402 | }
403 |
404 | .specimen-line .loading.visible {
405 | opacity: 1;
406 | }
407 |
408 | .specimen-line .no-words-found {
409 | position: absolute;
410 | top: 0;
411 | left: 0;
412 | width: 100%;
413 | height: 100%;
414 | display: flex;
415 | justify-content: center;
416 | align-items: center;
417 | white-space: nowrap;
418 | pointer-events: none;
419 | user-select: none;
420 | -webkit-user-select: none;
421 | }
422 |
423 | @keyframes fadeIn {
424 | 0% {
425 | opacity: 0;
426 | } 100% {
427 | opacity: 1;
428 | }
429 | }
430 |
431 | .specimen-line .loading .icon-spinning {
432 | margin-left: 0.35rem;
433 | }
434 |
435 | .line-right-col {
436 | grid-column: right;
437 | display: flex;
438 | justify-content: flex-end;
439 | align-items: flex-start;
440 | padding-top: 1rem;
441 | padding-bottom: 1rem;
442 | user-select: none;
443 | -webkit-user-select: none;
444 | }
445 |
446 | .update-button,
447 | .copy-button {
448 | position: relative;
449 | margin-left: 1rem;
450 | }
451 |
452 | .tooltip {
453 | font-size: 0.95rem;
454 | visibility: hidden;
455 | position: absolute;
456 | top: -2.1rem;
457 | background: var(--bg-color);
458 | padding: 0.2rem 0.35rem;
459 | border: 1px dotted;
460 | border-radius: 0.35rem;
461 | box-shadow: 1px 2px 2px rgba(0,0,0,0.25);
462 | white-space: nowrap;
463 | z-index: 1;
464 | pointer-events: none;
465 | }
466 |
467 | *:hover > .tooltip {
468 | visibility: visible;
469 | }
470 |
471 | .copy-button:hover .copy-tooltip,
472 | .update-button:hover .update-tooltip {
473 | visibility: visible;
474 | }
475 |
476 | .copy-tooltip,
477 | .update-tooltip {
478 | font-size: 0.95rem;
479 | visibility: hidden;
480 | position: absolute;
481 | top: -2.1rem;
482 | right: 0;
483 | background-color: var(--bg-color);
484 | background: var(--bg-color);
485 | padding: 0.2rem 0.35rem;
486 | border: 1px dotted;
487 | border-radius: 0.35rem;
488 | box-shadow: 1px 2px 2px rgba(0,0,0,0.25);
489 | white-space: nowrap;
490 | z-index: 1;
491 | pointer-events: none;
492 | }
493 |
494 | .new-line {
495 | grid-column: span 3;
496 | height: 5rem;
497 | display: flex;
498 | justify-content: center;
499 | align-items: center;
500 | border-bottom: 1px dotted;
501 | cursor: pointer;
502 | user-select: none;
503 | -webkit-user-select: none;
504 | }
505 |
506 | .new-line:hover .new-line-button {
507 | color: var(--green);
508 | }
509 |
510 | /* Font List */
511 |
512 | .font-items {
513 | position: relative;
514 | min-width: 0;
515 | padding-top: 0.9rem;
516 | padding-bottom: 0.9rem;
517 | }
518 |
519 | .font-items-scroller {
520 | display: flex;
521 | scroll-behavior: smooth;
522 | overflow-x: auto;
523 | overscroll-behavior-x: contain;
524 | -ms-overflow-style: none; /* IE and Edge */
525 | scrollbar-width: none; /* Firefox */
526 | }
527 |
528 | .font-items-scroller::-webkit-scrollbar {
529 | display: none;
530 | }
531 |
532 | .font-items-scroller.start {
533 | mask-image: linear-gradient(to left, rgb(0,0,0,0) 0%, rgb(0,0,0,1) 18%, rgb(0,0,0,1) 20%);
534 | }
535 |
536 | .font-items-scroller.middle {
537 | mask-image: linear-gradient(to left, rgb(0,0,0,0) 0%, rgb(0,0,0,1) 18%, rgb(0,0,0,1) 82%, rgb(0,0,0,0) 100%);
538 | }
539 |
540 | .font-items-scroller.end {
541 | mask-image: linear-gradient(to right, rgb(0,0,0,0) 0%, rgb(0,0,0,1) 18%, rgb(0,0,0,1) 20%);
542 | }
543 |
544 | .scroll-left-button {
545 | position: absolute;
546 | top: 0;
547 | height: 100%;
548 | display: flex;
549 | padding-right: 0.5rem;
550 | justify-content: flex-start;
551 | align-items: center;
552 | z-index: 1;
553 | }
554 |
555 | .scroll-right-button {
556 | position: absolute;
557 | top: 0;
558 | right: 0;
559 | height: 100%;
560 | display: flex;
561 | padding-left: 0.5rem;
562 | justify-content: flex-end;
563 | align-items: center;
564 | z-index: 1;
565 | }
566 |
567 | .font-item {
568 | white-space: nowrap;
569 | font-weight: 700;
570 | border: 1px solid var(--bg-color);
571 | user-select: none;
572 | -webkit-user-select: none;
573 | padding: 0.1rem 0.5rem;
574 | border-radius: 1rem;
575 | cursor: default;
576 | }
577 |
578 | .font-item:hover,
579 | .font-item.drag-clone {
580 | border: dotted 1px;
581 | background: var(--bg-color);
582 | color: var(--green);
583 | }
584 |
585 | .font-item.drag-clone {
586 | position: fixed;
587 | left: 0;
588 | pointer-events: none;
589 | }
590 |
591 | .font-item[data-dragged] {
592 | visibility: hidden;
593 | }
594 |
595 | .font-item.loading {
596 | color: #A0A0A0;
597 | }
598 |
599 | .font-item-label {
600 | margin-right: 0.5rem;
601 | }
602 |
603 | .font-item-remove {
604 | cursor: pointer;
605 | }
606 |
607 | .icon-spinning {
608 | animation: spinning 2s linear infinite;
609 | }
610 |
611 | .drop-placeholder {
612 | background: var(--green);
613 | width: 1rem;
614 | margin-right: 1rem;
615 | }
616 |
617 | @keyframes spinning {
618 | from {
619 | transform: rotate(0deg);
620 | }
621 | to {
622 | transform: rotate(360deg);
623 | }
624 | }
625 |
626 |
627 | /* Inputs */
628 |
629 | input:focus {
630 | outline: 0;
631 | box-shadow: 0 0 0 2px var(--grey);
632 | border-radius: 1px;
633 | }
634 |
635 | .line-count {
636 | white-space: nowrap;
637 | }
638 |
639 | .line-count-label {
640 | margin-right: 1rem;
641 | }
642 |
643 | .line-count-input {
644 | width: 2ch;
645 | text-align: center;
646 | display: inline-block;
647 | margin-left: 0.5rem;
648 | margin-right: 0.5rem;
649 | }
650 |
651 | .size-input {
652 | position: relative;
653 | display: flex;
654 | align-items: start;
655 | margin-right: 3rem;
656 | }
657 |
658 | .size-input-wrapper {
659 | display: flex;
660 | align-items: start;
661 | }
662 |
663 | .size-input-global {
664 | padding-top: 0;
665 | }
666 |
667 | .size-input.disabled button {
668 | cursor: default;
669 | }
670 |
671 | .size-input.disabled button:hover {
672 | color: inherit;
673 | }
674 |
675 | .size-input input {
676 | width: 5ch;
677 | text-align: center;
678 | display: inline-block;
679 | margin-left: 0.5rem;
680 | margin-right: 0.5rem;
681 | user-select: none;
682 | -webkit-user-select: none;
683 | }
684 |
685 | .size-input-lock {
686 | position: absolute;
687 | right: -1rem;
688 | cursor: pointer;
689 | }
690 |
691 | .width-input {
692 | display: flex;
693 | position: relative;
694 | align-items: center;
695 | justify-content: center;
696 | user-select: none;
697 | -webkit-user-select: none;
698 | }
699 |
700 | .width-input::before {
701 | content: '⇤';
702 | position: absolute;
703 | left: 0;
704 | }
705 |
706 | .width-input::after {
707 | content: '⇥';
708 | position: absolute;
709 | right: 0;
710 | }
711 |
712 | .width-input input {
713 | position: relative;
714 | border: none;
715 | width: 8ch;
716 | text-align: center;
717 | margin-left: 0.5rem;
718 | margin-right: 0.5rem;
719 | }
720 |
721 | .width-input input.small {
722 | top: -1.5rem;
723 | }
724 |
725 | .width-input-line {
726 | height: 0.75px;
727 | background: currentColor;
728 | flex: 1;
729 | }
730 |
731 | .width-input-handle.left {
732 | width: 0.5rem;
733 | height: 1rem;
734 | position: absolute;
735 | left: 0;
736 | cursor: ew-resize;
737 | z-index: 1;
738 | }
739 |
740 | .width-input-handle.right {
741 | width: 0.5rem;
742 | height: 1rem;
743 | position: absolute;
744 | right: 0;
745 | cursor: ew-resize;
746 | z-index: 1;
747 | }
748 |
749 | .width-input-line-cap-left {
750 | width: 0;
751 | }
752 |
753 | .width-input-line-cap-right {
754 | position: relative;
755 | left: -100%;
756 | }
757 |
758 | .size-slider {
759 | display: flex;
760 | align-items: center;
761 | }
762 |
763 | .size-slider label {
764 | margin-right: 0.5rem;
765 | }
766 |
767 | .select-wrapper {
768 | position: relative;
769 | padding-right: 0.75rem;
770 | white-space: nowrap;
771 | display: inline-block;
772 | cursor: pointer;
773 | }
774 |
775 | .select-wrapper:not(.disabled):hover {
776 | color: var(--green);
777 | }
778 |
779 | select {
780 | appearance: none;
781 | cursor: pointer;
782 | }
783 |
784 | select[disabled] {
785 | cursor: inherit;
786 | }
787 |
788 | .select-wrapper::after {
789 | content: '▿';
790 | position: absolute;
791 | right: 0;
792 | }
793 |
794 | .case-select {
795 | white-space: nowrap;
796 | }
797 |
798 | .case-select-lock {
799 | position: relative;
800 | margin-right: 0.5rem;
801 | display: inline-block;
802 | }
803 |
804 | .font-select {
805 | position: relative;
806 | margin-right: 2rem;
807 | white-space: nowrap;
808 | }
809 |
810 | .font-select-lock {
811 | position: absolute;
812 | right: -1rem;
813 | top: 0;
814 | }
815 |
816 | .menu-container {
817 | white-space: nowrap;
818 | position: relative;
819 | user-select: none;
820 | -webkit-user-select: none;
821 | }
822 |
823 | .menu {
824 | position: absolute;
825 | top: 1.5rem;
826 | left: -1rem;
827 | background: var(--bg-color);
828 | border: 1px dotted;
829 | border-radius: 0.35rem;
830 | box-shadow: 1px 2px 2px rgba(0,0,0,0.25);
831 | z-index: 3;
832 | max-height: calc(100vh - 5.5rem);
833 | overflow: auto;
834 | }
835 |
836 | .submenu {
837 | padding-top: 0rem;
838 | padding-bottom: 0rem;
839 | }
840 |
841 | .submenu > * {
842 | box-sizing: border-box;
843 | width: 100%;
844 | padding-left: 1rem;
845 | padding-right: 1rem;
846 | }
847 |
848 | .submenu-header {
849 | padding-top: 0.55rem;
850 | padding-bottom: 0.5rem;
851 | position: relative;
852 | display: flex;
853 | justify-content: space-between;
854 | position: sticky;
855 | top: 0;
856 | background: var(--bg-color);
857 | z-index: 1;
858 | }
859 |
860 | .submenu-header::before {
861 | content: "";
862 | position: absolute;
863 | left: 0;
864 | width: 100%;
865 | top: -1px;
866 | border-bottom: 1px dotted;
867 | border-color: var(--fg-color);
868 | }
869 |
870 | .submenu-header::after {
871 | content: "";
872 | position: absolute;
873 | left: 0;
874 | width: 100%;
875 | bottom: -1px;
876 | border-bottom: 1px dotted;
877 | border-color: var(--fg-color);
878 | }
879 |
880 | .submenu-toggle {
881 | cursor: pointer;
882 | }
883 |
884 | .submenu-toggle:hover {
885 | color: var(--green);
886 | }
887 |
888 | .submenu-content {
889 | overflow: hidden;
890 | transition: max-height 0.4s;
891 | }
892 |
893 | .submenu-content::before {
894 | content: "";
895 | display: block;
896 | padding-top: 0.75rem;
897 | }
898 |
899 | .submenu-content::after {
900 | content: "";
901 | display: block;
902 | padding-bottom: 0.75rem;
903 | }
904 |
905 | .menu-update {
906 | text-align: right;
907 | border-top: 1px dotted;
908 | padding: 0.55rem 1rem 0.5rem;
909 | position: sticky;
910 | bottom: 0;
911 | background: var(--bg-color);
912 | z-index: 1;
913 | }
914 |
915 | .menu-update.disabled button {
916 | cursor: default;
917 | }
918 |
919 | .menu-update.disabled button:hover {
920 | color: inherit;
921 | }
922 |
923 | .checkbox {
924 | display: flex;
925 | margin-bottom: 0.15rem;
926 | }
927 |
928 | input[type="checkbox"] {
929 | appearance: none;
930 | display: none;
931 | }
932 |
933 | .checkbox label {
934 | cursor: pointer;
935 | }
936 |
937 | .checkbox:hover {
938 | color: var(--green);
939 | }
940 |
941 | input[type="checkbox"] + label::before {
942 | content: '☐';
943 | margin-right: 0.5rem;
944 | }
945 |
946 | input[type="checkbox"]:checked + label::before {
947 | content: '☒';
948 | margin-right: 0.5rem;
949 | }
950 |
951 | .checkbox label {
952 | width: 100%;
953 | padding-left: 0.25rem;
954 | white-space: nowrap;
955 | }
956 |
957 | .feature-tag {
958 | font-family: monospace;
959 | font-size: 11px;
960 | position: relative;
961 | top: -1px;
962 | text-transform: uppercase;
963 | margin-right: 1em;
964 | }
965 |
966 | /* About */
967 |
968 | .about {
969 | position: absolute;
970 | width: 100%;
971 | top: 0;
972 | bottom: 0;
973 | background: var(--bg-color);
974 | visibility: hidden;
975 | opacity: 0;
976 | padding-top: 3.285rem;
977 | display: grid;
978 | grid-template-columns: 2fr 3fr 1fr 1fr 1fr;
979 | grid-column-gap: 2rem;
980 | transition: visibility 175ms, opacity 175ms, background 0.1s;
981 | z-index: 2;
982 | }
983 |
984 | .about.open {
985 | visibility: visible;
986 | opacity: 1;
987 | }
988 |
989 | .about svg {
990 | max-width: 100%;
991 | height: auto;
992 | }
993 |
994 | .about-text {
995 | grid-column: span 3;
996 | display: grid;
997 | grid-column-gap: 2rem;
998 | grid-template-columns: 3fr 1fr 1fr;
999 | grid-auto-rows: min-content
1000 | }
1001 |
1002 | .about-text p {
1003 | max-width: 51rem;
1004 | margin-bottom: 2rem;
1005 | grid-column: span 3;
1006 | }
1007 |
1008 | .about-text p.col-1 {
1009 | grid-column: span 1;
1010 | }
1011 |
1012 | .donation-btn {
1013 | border: 1px dotted;
1014 | border-radius: 7px;
1015 | padding: 0 8px 2px 11px;
1016 | display: inline-block;
1017 | margin-top: 8px;
1018 | }
1019 |
1020 | .donation-btn::before {
1021 | content: "$";
1022 | font-size: 14px;
1023 | position: relative;
1024 | top: -4px;
1025 | margin-right: 6px;
1026 | }
1027 |
1028 | @media screen and (max-width: 430px) {
1029 | .about {
1030 | padding-top: 0;
1031 | display: flex;
1032 | flex-wrap: wrap;
1033 | }
1034 |
1035 | .about svg {
1036 | max-width: 50%;
1037 | stroke-width: 2;
1038 | }
1039 |
1040 | .about-text p {
1041 | margin-bottom: 1rem;
1042 | }
1043 |
1044 | .about-text p.col-1 {
1045 | grid-column: span 2;
1046 | }
1047 | }
--------------------------------------------------------------------------------
/css/reset.css:
--------------------------------------------------------------------------------
1 | h1, h2, h3, h4, h5, h6 {
2 | margin: 0;
3 | font-size: inherit;
4 | font-weight: inherit;
5 | }
6 |
7 | fieldset,
8 | p {
9 | border: 0;
10 | padding: 0;
11 | margin: 0;
12 | }
13 |
14 | a, a:hover, a:active, a:visited {
15 | color: inherit;
16 | text-decoration: none;
17 | }
18 |
19 | em {
20 | font-style: normal;
21 | }
22 |
23 | button {
24 | color: inherit;
25 | font-size: inherit;
26 | font-family: inherit;
27 | margin: 0;
28 | padding: 0;
29 | box-shadow: none;
30 | border: none;
31 | background-color: transparent;
32 | }
33 |
34 | select {
35 | background: transparent;
36 | color: inherit;
37 | border: 0;
38 | padding: 0;
39 | font-family: inherit;
40 | font-size: inherit;
41 | }
42 |
43 | input {
44 | background: inherit;
45 | color: inherit;
46 | padding: 0;
47 | font-size: inherit;
48 | font-family: inherit;
49 | line-height: inherit;
50 | }
51 |
52 | input[type="number"],
53 | input[type="text"] {
54 | border: 0;
55 | }
56 |
57 | input[type=number] {
58 | -moz-appearance: textfield;
59 | appearance: textfield;
60 | }
61 |
62 | input[type=number]::-webkit-inner-spin-button,
63 | input[type=number]::-webkit-outer-spin-button {
64 | -webkit-appearance: none;
65 | margin: 0;
66 | }
--------------------------------------------------------------------------------
/fonts/PlombSans-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxesnee/stack-and-justify/fb54fe697996d5d5592ad5d98e0d632213b3417d/fonts/PlombSans-Bold.woff2
--------------------------------------------------------------------------------
/fonts/PlombSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxesnee/stack-and-justify/fb54fe697996d5d5592ad5d98e0d632213b3417d/fonts/PlombSans-Regular.woff2
--------------------------------------------------------------------------------
/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxesnee/stack-and-justify/fb54fe697996d5d5592ad5d98e0d632213b3417d/images/screenshot.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Stack & Justify
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
44 |
45 |
--------------------------------------------------------------------------------
/js/AppState.js:
--------------------------------------------------------------------------------
1 | export const AppState = (function() {
2 | return {
3 | showAbout: false
4 | }
5 | })();
--------------------------------------------------------------------------------
/js/Feature.js:
--------------------------------------------------------------------------------
1 | import { generateUID } from "./Helpers.js";
2 |
3 | // List of user controlled features that should be activated by default
4 | const defaultFeatures = ['liga', 'clig', 'calt'];
5 |
6 | export function Feature(tag, name) {
7 | const id = generateUID();
8 | let selected = defaultFeatures.includes(tag) ? true : false;
9 | let fontIds = [];
10 |
11 | return {
12 | id,
13 | tag,
14 | name,
15 | selected,
16 | fontIds
17 | }
18 | }
--------------------------------------------------------------------------------
/js/Filter.js:
--------------------------------------------------------------------------------
1 | import { generateUID } from './Helpers.js';
2 |
3 | export const Filter = function(name, label, apply) {
4 | const id = generateUID();
5 | return {
6 | id,
7 | name,
8 | label,
9 | apply
10 | }
11 | }
--------------------------------------------------------------------------------
/js/Filters.js:
--------------------------------------------------------------------------------
1 | import { Filter } from './Filter.js';
2 |
3 | export const Filters = [
4 | Filter('lowercase', 'Lowercase', (str) => str.toLowerCase()),
5 | Filter('uppercase', 'Uppercase', (str) => str.toUpperCase()),
6 | Filter('capitalised', 'Capitalised', (str) => str[0].toUpperCase() + str.slice(1))
7 | ];
8 |
9 |
--------------------------------------------------------------------------------
/js/Font.js:
--------------------------------------------------------------------------------
1 | import { Words } from "./Words.js";
2 | import { WordGenerator } from "./wordgenerator/WordGenerator.js";
3 | import { Layout } from "./Layout.js";
4 | import { generateUID, Computed } from "./Helpers.js";
5 |
6 | export const Font = function(name, data, info) {
7 | const id = generateUID();
8 | const fontFaceName = info.fileName;
9 | const features = [];
10 | const fontFeatureSettings = Computed(() => generateFontFeatureSettings(features));
11 | const displayFeatureSettings = Computed(() => fontFeatureSettings.val);
12 | const wordGenerator = WordGenerator(fontFaceName, data);
13 | let isLoading = true;
14 |
15 | async function load() {
16 | const fontFace = new FontFace(fontFaceName, data);
17 | document.fonts.add(fontFace);
18 |
19 | await fontFace.load();
20 |
21 | update();
22 | }
23 |
24 | async function update() {
25 | isLoading = true;
26 | const words = await Words.get();
27 | fontFeatureSettings.update();
28 |
29 | try {
30 | await wordGenerator.sort(words, fontFeatureSettings.val);
31 | } catch (error) {
32 | console.log(error);
33 | }
34 |
35 | displayFeatureSettings.update();
36 |
37 | // Dispatch event
38 | const event = new CustomEvent("font-loaded", {detail: {font}});
39 | window.dispatchEvent(event);
40 |
41 | isLoading = false;
42 | }
43 |
44 | const font = {
45 | name,
46 | fontFaceName,
47 | data,
48 | info,
49 | features,
50 | fontFeatureSettings: displayFeatureSettings,
51 | id,
52 | load,
53 | update,
54 | wordGenerator,
55 | get isLoading() {
56 | return isLoading;
57 | },
58 | }
59 |
60 | return font;
61 | }
62 |
63 | function generateFontFeatureSettings(features) {
64 | let str = "";
65 | let featureStrings = [];
66 |
67 | for (let feature of features) {
68 | if (feature.selected) {
69 | featureStrings.push(`"${feature.tag}" on`);
70 | } else {
71 | featureStrings.push(`"${feature.tag}" off`);
72 | }
73 | }
74 |
75 | str = featureStrings.join(',');
76 | return str;
77 | }
--------------------------------------------------------------------------------
/js/Fonts.js:
--------------------------------------------------------------------------------
1 | import { parse, getFontInfo } from './miniotparser/MiniOTParser.js';
2 | import { Font } from './Font.js';
3 | import { Feature } from './Feature.js';
4 | import { generateUID } from './Helpers.js';
5 |
6 | export const Fonts = (function() {
7 | const list = [];
8 |
9 | function add(font) {
10 | // Group the font by family name
11 | let family = list.find(family => family.name === font.info.familyName);
12 |
13 | // If the family group doesn't already exist, create it
14 | if (!family) {
15 | family = {
16 | id: generateUID(font.info.familyName),
17 | name: font.info.familyName,
18 | list: [],
19 | features: font.info.features.map(feature => Feature(feature.tag, feature.name))
20 | };
21 | list.push(family);
22 | }
23 |
24 | // Add the features that were not already present in the list
25 | // We allow for duplicate features only if they have different names
26 | // This allow for multiple version of the same stylistic sets inside a family
27 | font.info.features.forEach(featureInfo => {
28 | let feature = family.features.find(_feature => _feature.name === featureInfo.name);
29 |
30 | if (!feature) {
31 | feature = Feature(featureInfo.tag, featureInfo.name);
32 | family.features.push(feature);
33 | }
34 | // We also add a reference to the feature in the font object
35 | font.features.push(feature);
36 | });
37 |
38 | // Push the font in the family list
39 | family.list.push(font);
40 |
41 | // Sort by style
42 | sortFonts(family.list);
43 |
44 | // Start sorting the dictionnary
45 | font.load();
46 |
47 | // Dispatch event
48 | const event = new CustomEvent("font-added", {detail: {font: font}});
49 | window.dispatchEvent(event);
50 | }
51 |
52 | function find(fontId) {
53 | for (let family of list) {
54 | for (let font of family.list) {
55 | if (font.id === fontId) {
56 | return font;
57 | }
58 | }
59 | }
60 | }
61 |
62 | // Update every font in the list
63 | function updateAll() {
64 | list.forEach(family => {
65 | family.list.forEach(font => font.update());
66 | });
67 | }
68 |
69 | // Get form data from the Features menu and activate/desactivate
70 | // the corresponding features in the list.
71 | // Then, update the fonts that includes one the updated features.
72 | function updateFeatures(formData) {
73 | for (let family of list) {
74 | if (formData.has(family.id)) {
75 | const updatedFeatures = [];
76 |
77 | const selectedFeatures = formData.getAll(family.id);
78 | for (let feature of family.features) {
79 | if (selectedFeatures.includes(feature.id) && !feature.selected) {
80 | // The feature has been activated
81 | feature.selected = true;
82 | updatedFeatures.push(feature);
83 |
84 | } else if (!selectedFeatures.includes(feature.id) && feature.selected) {
85 | // The feature has been desactivated
86 | feature.selected = false;
87 | updatedFeatures.push(feature);
88 |
89 | }
90 | }
91 |
92 | for (let font of family.list) {
93 | let needsUpdate = false;
94 | for (let feature of updatedFeatures) {
95 | if (font.features.includes(feature)) {
96 | needsUpdate = true;
97 | }
98 | }
99 | if (needsUpdate) font.update();
100 | }
101 | }
102 | }
103 | }
104 |
105 | return {
106 | list,
107 | add,
108 | find,
109 | updateAll,
110 | updateFeatures,
111 | get length() {
112 | return list.reduce((acc, curr) => acc + curr.list.length, 0);
113 | }
114 | }
115 |
116 | })();
117 |
118 | export function handleFontFiles(files) {
119 | const acceptedExtensions = /^.*\.(ttf|otf|woff|woff2)$/i;
120 |
121 | files = Array.from(files);
122 | const validFiles = files.filter(file => file.name.match(acceptedExtensions));
123 |
124 | const loadedFonts = validFiles.map(loadFontFile);
125 |
126 | Promise.all(loadedFonts).then(fonts => {
127 | sortFonts(fonts);
128 |
129 | fonts.forEach(Fonts.add);
130 | });
131 | }
132 |
133 | export function loadFontFile(file) {
134 |
135 | return new Promise((resolve, reject) => {
136 | // Removes file extension from name
137 | let fileName = file.name.substring(0, file.name.lastIndexOf('.'));
138 | // Replace any non alpha numeric characters with -
139 | fileName = fileName.replace(/\W+/g, "-");
140 | // Remove leading digits in the filename
141 | fileName = fileName.replace(/^[0-9]+/g, '');
142 |
143 | const reader = new FileReader();
144 |
145 | reader.onloadend = function(e) {
146 | const fontInfo = getFontInfo(parse(e.target.result), fileName);
147 | // Check if a font with the same name does not exists already
148 | if (Fonts.find(font => font.name === fontInfo.fullName)) {
149 | reject();
150 | }
151 |
152 | resolve(Font(fontInfo.fullName, e.target.result, fontInfo));
153 | }
154 |
155 | reader.readAsArrayBuffer(file);
156 | });
157 | }
158 |
159 | export function sortFonts(list) {
160 | if (list.length <= 1) return list;
161 |
162 | const sortedFonts = list;
163 |
164 | // Sort regular/italic pairs
165 | sortedFonts.sort((fontA, fontB) => {
166 | if (fontA.info.isItalic && !fontB.info.isItalic) {
167 | return 1
168 | } else if (!fontA.info.isItalic && fontB.info.isItalic) {
169 | return -1
170 | } else {
171 | return 0;
172 | }
173 | });
174 |
175 | // Sort by weight
176 | sortedFonts.sort((fontA, fontB) => {
177 | return fontA.info.weightClass - fontB.info.weightClass;
178 | });
179 |
180 | // Sort by width
181 | sortedFonts.sort((fontA, fontB) => {
182 | return fontA.info.widthClass - fontB.info.widthClass;
183 | });
184 |
185 | // Sort by family name
186 | sortedFonts.sort((fontA, fontB) => {
187 | return fontA.info.familyName.localeCompare(fontB.info.familyName);
188 | });
189 |
190 | return sortedFonts;
191 | }
--------------------------------------------------------------------------------
/js/Helpers.js:
--------------------------------------------------------------------------------
1 | export function generateUID(str) {
2 | if (str && str.length) {
3 | return generateUIDFromString(str);
4 | }
5 |
6 | let firstPart = (Math.random() * 46656) | 0;
7 | let secondPart = (Math.random() * 46656) | 0;
8 | firstPart = ("000" + firstPart.toString(36)).slice(-3);
9 | secondPart = ("000" + secondPart.toString(36)).slice(-3);
10 | return firstPart + secondPart;
11 | }
12 |
13 |
14 | // Generate hash code from string
15 | export function generateUIDFromString(str) {
16 | var hash = 0,
17 | i, chr;
18 | if (str.length === 0) return hash;
19 | for (i = 0; i < str.length; i++) {
20 | chr = str.charCodeAt(i);
21 | hash = ((hash << 5) - hash) + chr;
22 | hash |= 0; // Convert to 32bit integer
23 | }
24 | return hash;
25 | }
26 |
27 |
28 | export function Box(val) {
29 | let _val = val;
30 | let callbacks = [];
31 |
32 | return {
33 | get val() {
34 | return _val;
35 | },
36 | set val(newVal) {
37 | _val = newVal;
38 | callbacks.forEach(callback => callback());
39 | },
40 | onchange(callback) {
41 | callbacks.push(callback);
42 | }
43 | }
44 | }
45 |
46 | export function Computed(fn) {
47 | let _val = fn();
48 | let callbacks = [];
49 | return {
50 | get val() {
51 | return _val;
52 | },
53 | update() {
54 | const newVal = fn();
55 | if (_val !== newVal) {
56 | _val = newVal;
57 | callbacks.forEach(callback => callback());
58 | }
59 | },
60 | onchange(callback) {
61 | callbacks.push(callback);
62 | },
63 | dependsOn(...dependencies) {
64 | dependencies.forEach(dependency => {
65 | if (typeof dependency.onchange === 'function') {
66 | dependency.onchange(this.update);
67 | }
68 | });
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/js/Layout.js:
--------------------------------------------------------------------------------
1 | import { Line } from "./Line.js";
2 | import { Size } from "./Size.js";
3 | import { Filters } from "./Filters.js";
4 | import { Box } from "./Helpers.js";
5 |
6 | export const defaultWidth = Size('15cm');
7 | export const defaultSize = Size('60pt');
8 | export const defaultFilter = Filters[2];
9 |
10 | export const Layout = (function() {
11 | let lines = [];
12 | let width = Size(defaultWidth.get());
13 | let size = Size(defaultSize.get());
14 | let sizeLocked = Box(true);
15 | let filter = Box(defaultFilter);
16 | let filterLocked = Box(true);
17 | let font = Box(null);
18 | let fontLocked = Box(false);
19 |
20 | window.addEventListener('font-added', (e) => {
21 | // If there's was no font before, select the one that's been added
22 | if (font.val == null) {
23 | font.val = e.detail.font;
24 | }
25 | addLine(e.detail.font, defaultSize, defaultFilter);
26 | m.redraw();
27 | });
28 |
29 | function copyText() {
30 | navigator.clipboard.writeText(lines.map(line => line.text.val).join('\n'));
31 | }
32 |
33 | async function update() {
34 | lines.forEach(line => {line.update()});
35 | }
36 |
37 | function addLine(_font, _size, _filter) {
38 | if (!_font && !_size && !_filter) {
39 | if (lines.length) {
40 | const lastLine = lines[lines.length-1];
41 | _font = lastLine.font.val;
42 | _size = lastLine.size;
43 | _filter = lastLine.filter.val;
44 | } else {
45 | _font = font.val;
46 | _size = defaultSize;
47 | _filter = defaultFilter;
48 | }
49 | }
50 | lines.push(Line(_font, _size, _filter));
51 | }
52 |
53 | function moveLine(line, to) {
54 | const from = lines.indexOf(line);
55 | if (from === -1 || to === from) return;
56 |
57 | const target = lines[from];
58 | const increment = to < from ? -1 : 1;
59 |
60 | for(let k = from; k != to; k += increment){
61 | lines[k] = lines[k + increment];
62 | }
63 | lines[to] = target;
64 | }
65 |
66 | function getLine(id) {
67 | return lines.find(line => line.id === id) || null;
68 | }
69 |
70 | function indexOf(id) {
71 | return lines.indexOf(getLine(id));
72 | }
73 |
74 | function removeLine(id) {
75 | if (id === undefined) {
76 | lines.pop();
77 | } else {
78 | const index = lines.indexOf(lines.find(line => line.id === id));
79 | lines.splice(index, 1);
80 | }
81 | }
82 |
83 | function clear() {
84 | lines.length = 0;
85 | }
86 |
87 | function textAlreadyUsed(str) {
88 | return lines.find(line => line.text.val === str) ? true : false;
89 | }
90 |
91 | return {
92 | lines,
93 | width,
94 | size,
95 | filter,
96 | font,
97 | sizeLocked,
98 | filterLocked,
99 | fontLocked,
100 | addLine,
101 | removeLine,
102 | getLine,
103 | moveLine,
104 | indexOf,
105 | update,
106 | copyText,
107 | clear,
108 | textAlreadyUsed,
109 |
110 | }
111 | })();
--------------------------------------------------------------------------------
/js/Line.js:
--------------------------------------------------------------------------------
1 | import { Size } from './Size.js';
2 | import { Layout } from './Layout.js';
3 | import { generateUID, Box, Computed } from './Helpers.js';
4 |
5 | export function Line(_font, _size, _filter) {
6 | const id = generateUID();
7 | let font = Box(_font);
8 | let size = Size(_size.get());
9 | let filter = Box(_filter);
10 |
11 | const outputFont = Computed(() => Layout.fontLocked.val ? Layout.font.val : font.val);
12 | outputFont.dependsOn(Layout.font, Layout.fontLocked, font);
13 |
14 | const outputSize = Computed(() => Layout.sizeLocked.val ? Layout.size.getIn('px') : size.getIn('px'));
15 | outputSize.dependsOn(Layout.sizeLocked, Layout.size, size);
16 |
17 | const outputFilter = Computed(() => Layout.filterLocked.val ? Layout.filter.val : filter.val);
18 | outputFilter.dependsOn(Layout.filter, Layout.filterLocked, filter);
19 |
20 | const text = Computed(() => {
21 | const textOptions = outputFont.val.wordGenerator.getWords(outputSize.val, Layout.width.getIn('px'), outputFilter.val, Layout.lines.length);
22 | return textOptions.find(option => !Layout.textAlreadyUsed(option)) || "";
23 | });
24 | text.dependsOn(Layout.width, outputFont, outputSize, outputFilter);
25 |
26 | window.addEventListener('font-loaded', (e) => {
27 | if (e.detail.font === outputFont.val) {
28 | text.update();
29 | m.redraw();
30 | }
31 | });
32 |
33 | function remove() {
34 | Layout.removeLine(id);
35 | }
36 |
37 | function copyText() {
38 | navigator.clipboard.writeText(text.val);
39 | }
40 |
41 | return {
42 | id,
43 | font,
44 | size,
45 | filter,
46 | outputFont,
47 | outputSize,
48 | outputFilter,
49 | text,
50 | update: text.update,
51 | remove,
52 | copyText
53 | }
54 | }
--------------------------------------------------------------------------------
/js/Size.js:
--------------------------------------------------------------------------------
1 | export const Size = function(_str) {
2 | let value, unit;
3 | let callbacks = [];
4 |
5 | ({value, unit} = processStr(_str));
6 |
7 | function processStr(str) {
8 | if (typeof str === 'string' && str !== ""){
9 | str = str.replace(',', '.');
10 | var split = str.match(/^([-.\d]+(?:\.\d+)?)(.*)$/);
11 | return {'value': parseFloat(split[1].trim()), 'unit': split[2].trim() || unit};
12 | }
13 | else {
14 | return { 'value': value, 'unit': unit };
15 | }
16 | }
17 |
18 | function increment() {
19 | value += 1;
20 | onchange();
21 | }
22 |
23 | function decrement() {
24 | value -= 1;
25 | onchange();
26 | }
27 |
28 | function set(str) {
29 | ({value, unit} = processStr(str));
30 | onchange();
31 | }
32 |
33 | function get() {
34 | return `${parseFloat(value.toFixed(2))}${unit}`;
35 | }
36 |
37 | function getIn(targetUnit) {
38 | return convert(value, unit).to(targetUnit);
39 | }
40 |
41 | function setIn(srcUnit, newValue) {
42 | value = convert(newValue, srcUnit).to(unit);
43 | onchange();
44 | }
45 |
46 | function convert(value, unit) {
47 | const ratios = {
48 | 'cm': 37.8,
49 | 'mm': 3.78,
50 | 'in': 96,
51 | 'pt': 1.333,
52 | 'pc': 16,
53 | 'px': 1
54 | }
55 |
56 | const valueInPixel = value * ratios[unit];
57 |
58 | return {
59 | to: function(targetUnit) {
60 | return valueInPixel / ratios[targetUnit];
61 | }
62 | }
63 | }
64 |
65 | function onchange() {
66 | callbacks.forEach(callback => callback());
67 | }
68 |
69 | return {
70 | get value() {
71 | return value;
72 | },
73 | get unit() {
74 | return unit
75 | },
76 | get,
77 | getIn,
78 | set,
79 | setIn,
80 | increment,
81 | decrement,
82 | onchange(callback) {
83 | callbacks.push(callback);
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/js/Words.js:
--------------------------------------------------------------------------------
1 | export const Words = (function() {
2 | const languages = [
3 | {
4 | name: 'catalan',
5 | label: 'Catalan',
6 | code: 'ca',
7 | selected: false
8 | }, {
9 | name: 'czech',
10 | label: 'Czech',
11 | code: 'cs',
12 | selected: false
13 | }, {
14 | name: 'danish',
15 | label: 'Danish',
16 | code: 'da',
17 | selected: false
18 | }, {
19 | name: 'dutch',
20 | label: 'Dutch',
21 | code: 'nl',
22 | selected: false
23 | }, {
24 | name: 'english',
25 | label: 'English',
26 | code: 'en',
27 | selected: true
28 | }, {
29 | name: 'finnish',
30 | label: 'Finnish',
31 | code: 'fi',
32 | selected: false
33 | }, {
34 | name: 'french',
35 | label: 'French',
36 | code: 'fr',
37 | selected: false
38 | }, {
39 | name: 'german',
40 | label: 'German',
41 | code: 'de',
42 | selected: false
43 | }, {
44 | name: 'hungarian',
45 | label: 'Hungarian',
46 | code: 'hu',
47 | selected: false
48 | }, {
49 | name: 'icelandic',
50 | label: 'Icelandic',
51 | code: 'is',
52 | selected: false
53 | }, {
54 | name: 'italian',
55 | label: 'Italian',
56 | code: 'it',
57 | selected: false
58 | }, {
59 | name: 'latin',
60 | label: 'Latin',
61 | code: 'la',
62 | selected: false
63 | }, {
64 | name: 'norwegian',
65 | label: 'Norwegian',
66 | code: 'no',
67 | selected: false
68 | }, {
69 | name: 'polish',
70 | label: 'Polish',
71 | code: 'pl',
72 | selected: false
73 | }, {
74 | name: 'slovak',
75 | label: 'Slovak',
76 | code: 'sk',
77 | selected: false
78 | }, {
79 | name: 'spanish',
80 | label: 'Spanish',
81 | code: 'es',
82 | selected: false
83 | }
84 | ];
85 |
86 | const sources = [
87 | {
88 | name: 'dictionary',
89 | label: 'Dictionary',
90 | selected: true,
91 | words: (function() {
92 | const obj = {};
93 | languages.forEach(language => {
94 | obj[language.name] = {
95 | url: `words/dictionaries/${language.name}.json`,
96 | list: null
97 | }
98 | });
99 | return obj;
100 | })()
101 | }, {
102 | name: 'wikipedia',
103 | label: 'Wikipedia article titles',
104 | selected: false,
105 | words: (function() {
106 | const obj = {};
107 | languages.forEach(language => {
108 | obj[language.name] = {
109 | url: `words/wikipedia/${language.code}_wikipedia.json`,
110 | list: null
111 | }
112 | });
113 | return obj;
114 | })()
115 | }
116 | ]
117 |
118 | function loadFile(url) {
119 | return fetch(url)
120 | .then(response => response.json())
121 | .then(data => data.words)
122 | .catch(error => console.error('Error loading JSON file:', error));
123 | }
124 |
125 | async function get() {
126 | let words = [];
127 | const promises = [];
128 | for (const source of sources.filter(source => source.selected)) {
129 | for (const language of languages.filter(lang => lang.selected)) {
130 | const listObject = source.words[language.name];
131 | if (listObject.list === null) {
132 | listObject.list = loadFile(listObject.url);
133 | }
134 | promises.push(listObject.list);
135 | listObject.list.then(list => {
136 | words = words.concat(list);
137 | });
138 |
139 | }
140 | }
141 | await Promise.all(promises);
142 | return words;
143 | }
144 |
145 |
146 | return {
147 | get,
148 | data: {
149 | languages,
150 | sources
151 | }
152 | };
153 |
154 | })();
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | import { Fonts } from "./Fonts.js";
2 | import { Header } from "./components/Header.js";
3 | import { Footer } from "./components/Footer.js";
4 | import { About } from "./components/About.js";
5 | import { DropZone } from "./components/DropZone.js";
6 | import { Specimen } from "./components/Specimen.js";
7 | import { SplashScreen } from "./components/SplashScreen.js";
8 |
9 | const root = document.querySelector('#app');
10 |
11 | const App = {
12 | view: function(vnode) {
13 | return [
14 | m(Header),
15 | m(DropZone),
16 | m('main.main',
17 | Fonts.length ? m(Specimen) : m(SplashScreen),
18 | m(About)
19 | ),
20 | m(Footer)
21 | ]
22 | }
23 | }
24 |
25 | m.mount(root, App);
--------------------------------------------------------------------------------
/js/components/About.js:
--------------------------------------------------------------------------------
1 | import { AppState } from "../AppState.js";
2 | import { SVGAnimation } from "./SVGAnimation.js";
3 |
4 | export function About(initialVnode) {
5 | return {
6 | view: function(vnode) {
7 | return m('section.about', {class: AppState.showAbout ? 'open' : ''},
8 | m(SVGAnimation, {src: 'svg/stack-and-justify-animation.svg', frames: 75}),
9 | m('div.about-text',
10 | m('p.t-big',
11 | m('em.bold', "Stack & Justify"),
12 | m('span', " is a tool to help create type specimens by finding words or phrases of the same width. It is free to use and distributed under GPLv3 license.")
13 | ),
14 | m('p.t-big', "Font files are not uploaded, they remain stored locally in your browser."),
15 | m('p.t-big',
16 | m('span', "For a similar tool, also check "),
17 | m('a.big-link', {target: '_blank', href: "https://workshop.mass-driver.com/waterfall"}, "Mass Driver’s Waterfall"),
18 | m('span', " from which this tool was inspired.")
19 | ),
20 | m('p.col-1',
21 | m('span', "If you want to support this project, you can make a donation via Paypal"),
22 | m('br'),
23 | m('a.t-big.donation-btn', {target: '_blank', href: "https://www.paypal.com/donate/?hosted_button_id=677KMWSBSRL3N"}, "Donate")
24 | ),
25 | ),
26 | )
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/js/components/CopyButton.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function CopyButton(initialVnode) {
4 | return {
5 | oncreate: function(vnode) {
6 | vnode.dom.querySelector('button').addEventListener('click', vnode.attrs.onclick);
7 | },
8 | view: function(vnode) {
9 | return m('div.copy-button',
10 | m('button', '📋'),
11 | m(Tooltip, {label: 'Copy line to clipboard', activeLabel: 'Copied'})
12 | )
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/js/components/CopyButtonGlobal.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function CopyButtonGlobal(initialVnode) {
4 | return {
5 | oncreate: function(vnode) {
6 | vnode.dom.querySelector('button').addEventListener('click', vnode.attrs.onclick);
7 | },
8 | view: function(vnode) {
9 | return m('div.copy-button',
10 | m('button', '📋'),
11 | m(Tooltip, {label: 'Copy all lines to clipboard', activeLabel: 'Copied'})
12 | )
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/js/components/DarkModeButton.js:
--------------------------------------------------------------------------------
1 | export function DarkModeButton(initialVnode) {
2 | return {
3 | view: function(vnode) {
4 | return m('button.dark-mode-btn', {
5 | onclick: () => { document.body.classList.toggle('dark') }
6 | }, "◒")
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/js/components/DeleteButton.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function DeleteButton(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.update-button',
7 | m('button', {onclick: vnode.attrs.onclick }, '🗑'),
8 | m(Tooltip, {label: 'Delete line'})
9 | )
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/js/components/DeleteButtonGlobal.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function DeleteButtonGlobal(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.update-button',
7 | m('button', {onclick: vnode.attrs.onclick },'🗑'),
8 | m(Tooltip, {label: 'Clear all lines'})
9 | )
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/js/components/DropZone.js:
--------------------------------------------------------------------------------
1 | import { Fonts, handleFontFiles } from "../Fonts.js";
2 | import { Font } from "../Font.js";
3 |
4 | export function DropZone(initialVnode) {
5 | return {
6 | oncreate: function(vnode) {
7 | let lastTarget = null;
8 |
9 | window.addEventListener('dragover', function(e) {
10 | e.preventDefault();
11 | e.dataTransfer.dropEffect = "copy";
12 | });
13 |
14 | window.addEventListener('dragenter', function(e) {
15 | lastTarget = e.target;
16 | e.dataTransfer.effectAllowed = "copy";
17 | vnode.dom.classList.add('active');
18 | });
19 |
20 | window.addEventListener('dragleave', function(e) {
21 | if(e.target === lastTarget || e.target === document) {
22 | vnode.dom.classList.remove('active');
23 | }
24 | });
25 |
26 | vnode.dom.addEventListener('drop', function(e) {
27 | e.preventDefault();
28 |
29 | let files = e.dataTransfer.files;
30 |
31 | handleFontFiles(files);
32 |
33 | vnode.dom.classList.remove('active');
34 | });
35 | },
36 | view: function(vnode) {
37 | return m('div.drop-zone')
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/js/components/FeaturesMenu.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Fonts } from "../Fonts.js";
3 | import { generateUID } from "../Helpers.js";
4 |
5 | export function FeaturesMenu(initialVnode) {
6 | let open = false;
7 | let needsUpdate = false;
8 |
9 | function update(e) {
10 | e.preventDefault();
11 |
12 | const formData = new FormData(e.target);
13 | Fonts.updateFeatures(formData);
14 |
15 | open = false;
16 | needsUpdate = false;
17 |
18 | m.redraw();
19 | }
20 |
21 | return {
22 | oncreate: function(vnode) {
23 | // Close the menu when the user clicks anywhere else
24 | document.addEventListener('click', (e) => {
25 | const menu = vnode.dom.querySelector('.menu');
26 | const btn = vnode.dom.querySelector('.menu-button');
27 |
28 | if (!menu.contains(e.target) && e.target !== btn && open) {
29 | open = false;
30 | m.redraw();
31 | }
32 | });
33 | },
34 | view: function(vnode) {
35 | const familiesWithFeatures = Fonts.list.filter(family => family.features.length);
36 | const disabled = familiesWithFeatures.length === 0;
37 |
38 | return m('div.menu-container.features',
39 | m('button.menu-button', {
40 | class: disabled ? "disabled" : "",
41 | disabled,
42 | onclick: () => { open = !open }
43 | }, "Features ▿"),
44 | m('form.menu', {onsubmit: update, style: {visibility: open ? 'visible' : 'hidden'}},
45 | familiesWithFeatures.map(family => {
46 | return m(FeaturesSubmenu, {
47 | key: family.id,
48 | family,
49 | open: true,
50 | onchange: () => { needsUpdate = true },
51 | });
52 | }),
53 | m('div.menu-update', {class: !needsUpdate ? "disabled" : ""},
54 | m('button.bold', {type: 'submit', disabled: !needsUpdate},'↻ Update')
55 | )
56 | )
57 | )
58 | }
59 | }
60 | }
61 |
62 | export function FeaturesSubmenu(initialVnode) {
63 | let open = initialVnode.attrs.open;
64 | let offsetHeight = null;
65 | let update = function() {};
66 |
67 | return {
68 | oncreate: function(vnode) {
69 | offsetHeight = vnode.dom.offsetHeight;
70 | },
71 | onupdate: function(vnode) {
72 | vnode.dom.querySelector('.submenu-content').style.maxHeight = open ? `${offsetHeight}px` : '0';
73 | },
74 | view: function(vnode) {
75 | const family = vnode.attrs.family;
76 | return m('fieldset.submenu',
77 | m('legend.submenu-header.submenu-toggle', {onclick: () => { open = !open }, class: open ? "open" : "closed"},
78 | m('span', family.name),
79 | m('span.submenu-toggle', "▿")
80 | ),
81 | m('div.submenu-content',
82 | family.features.map(feature => {
83 | return m(FeatureCheckbox, {
84 | key: feature.id,
85 | family,
86 | feature,
87 | onchange: vnode.attrs.onchange
88 | });
89 | })
90 | )
91 | );
92 | }
93 | }
94 | }
95 |
96 | function FeatureCheckbox(initialVnode) {
97 | let checked = initialVnode.attrs.feature.selected;
98 |
99 | return {
100 | view: function(vnode) {
101 | const family = vnode.attrs.family;
102 | const feature = vnode.attrs.feature;
103 | return m('div.checkbox',
104 | m('input', {name: family.id,
105 | type: 'checkbox',
106 | id: `${feature.tag}-${feature.id}`,
107 | value: feature.id,
108 | checked: checked,
109 | onchange: (e) => {checked = e.currentTarget.checked; vnode.attrs.onchange()},
110 | }
111 | ),
112 | m('label', {for: `${feature.tag}-${feature.id}`},
113 | m('span.feature-tag', feature.tag),
114 | m('span', feature.name)
115 | )
116 | )
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/js/components/FilterSelect.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Filters } from "../Filters.js";
3 |
4 | export function FilterSelect(initialVnode) {
5 | return {
6 | view: function(vnode) {
7 | const line = vnode.attrs.params;
8 | return m('div.select-wrapper', {class: Layout.filterLocked.val ? "disabled": ""},
9 | m('select.case-select', {
10 | disabled: Layout.filterLocked.val,
11 | onchange: (e) => {line.filter.val = Filters.find(filter => filter.id === e.currentTarget.value)},
12 | },
13 | Filters.map(filter => {
14 | return m('option', {value: filter.id, selected: line.filter.val.id === filter.id}, filter.label)
15 | })
16 | )
17 | )
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/js/components/FilterSelectGlobal.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Filters } from "../Filters.js";
3 | import { Tooltip } from './Tooltip.js';
4 |
5 | export function FilterSelectGlobal(initialVnode) {
6 | return {
7 | view: function(vnode) {
8 | return m('div.case-select',
9 | m('div.case-select-lock',
10 | m('button', {
11 | onclick: () => {Layout.filterLocked.val = !Layout.filterLocked.val}
12 | }, Layout.filterLocked.val ? '🔒' : '🔓'),
13 | m(Tooltip, {label: 'Apply to all lines'})
14 | ),
15 | m('div.select-wrapper', {class: !Layout.filterLocked.val ? "disabled" : ""},
16 | m('select.case-select', {
17 | disabled: !Layout.filterLocked.val,
18 | onchange: (e) => {Layout.filter.val = Filters.find(filter => filter.id === e.currentTarget.value)},
19 | },
20 | Filters.map(filter => {
21 | return m('option', {value: filter.id, selected: Layout.filter.val.id === filter.id}, filter.label)
22 | })
23 | )
24 | )
25 | )
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/js/components/FontInput.js:
--------------------------------------------------------------------------------
1 | import { Fonts, handleFontFiles } from "../Fonts.js";
2 | import { Font } from "../Font.js";
3 |
4 | export function FontInput(initialVnode) {
5 | return {
6 | oncreate: function(vnode) {
7 | const input = vnode.dom.querySelector('input[type="file"');
8 | vnode.dom.querySelector('a').addEventListener('click', (e) => {
9 | e.preventDefault();
10 | input.click();
11 | });
12 |
13 | input.addEventListener('change', (e) => {
14 | let files = input.files;
15 |
16 | handleFontFiles(files);
17 | });
18 | },
19 | view: function(vnode) {
20 | return m('form.drop-message',
21 | m('input', {type: 'file', id:'file-upload', multiple:'multiple', accept: 'font/otf, font/ttf, font/woff, font/woff2, .otf, .ttf, .woff, .woff2', style:{display: 'none'}}),
22 | m('span', 'Drop your fonts anywhere or '),
23 | m('label', {for: 'file-upload'},
24 | m('a.drop-btn', 'browse your computer ↗')
25 | )
26 | )
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/js/components/FontSelect.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Fonts } from "../Fonts.js";
3 |
4 | export function FontSelect(initialVnode) {
5 | return {
6 | oncreate: function(vnode) {
7 | // Get the width of the hidden label and update the width of the select
8 | const width = vnode.dom.querySelector('.font-select-hidden-label').offsetWidth;
9 | vnode.dom.querySelector('select').style.width = width + 'px';
10 | },
11 | onupdate: function(vnode) {
12 | // Get the width of the hidden label and update the width of the select
13 | const width = vnode.dom.querySelector('.font-select-hidden-label').offsetWidth;
14 | vnode.dom.querySelector('select').style.width = width + 'px';
15 | },
16 | view: function(vnode) {
17 | const line = vnode.attrs.params;
18 | return m('div.font-select',
19 | m('span.font-select-hidden-label', {style: {position: 'absolute', visibility: 'hidden'}}, line.font.val.name),
20 | m('div.select-wrapper', {class: Layout.fontLocked.val ? "disabled" : ""},
21 | m('select', {
22 | disabled: Layout.fontLocked.val,
23 | oninput: (e) => {line.font.val = Fonts.find(e.target.selectedOptions[0].value)},
24 | },
25 | Fonts.list.map(family => {
26 | return m('optgroup', {label: family.name},
27 | family.list.map(font => {
28 | return m('option', { value: font.id, selected: line.font.val.id == font.id}, font.name);
29 | })
30 | )
31 | })
32 | )
33 | )
34 | )
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/js/components/FontSelectGlobal.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Fonts } from "../Fonts.js";
3 | import { Tooltip } from "./Tooltip.js";
4 |
5 | export function FontSelectGlobal(initialVnode) {
6 | return {
7 | onupdate: function(vnode) {
8 | // Get the width of the hidden label and update the width of the select
9 | const width = vnode.dom.querySelector('.font-select-hidden-label').offsetWidth;
10 | vnode.dom.querySelector('select').style.width = width + 'px';
11 | },
12 | view: function(vnode) {
13 | const fontName = Layout.font.val ? Layout.font.val.name : '';
14 | return m('div.font-select',
15 | m('span.font-select-hidden-label', {style: {position: 'absolute', visibility: 'hidden'}}, fontName),
16 | m('div.select-wrapper', {class: !Layout.fontLocked.val ? "disabled" : ""},
17 | m('select', {
18 | disabled: !Layout.fontLocked.val,
19 | oninput: (e) => {Layout.font.val = Fonts.find(e.target.selectedOptions[0].value)},
20 | },
21 | Fonts.list.map(family => {
22 | return m('optgroup', {label: family.name},
23 | family.list.map(font => {
24 | return m('option', { value: font.id, selected: Layout.font.val.id == font.id}, font.name);
25 | })
26 | )
27 | })
28 | ),
29 | ),
30 | m('div.font-select-lock',
31 | m('button', {
32 | onclick: () => {Layout.fontLocked.val = !Layout.fontLocked.val}
33 | }, Layout.fontLocked.val ? '🔒' : '🔓'),
34 | m(Tooltip, {label: 'Apply to all lines'})
35 | )
36 | )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/js/components/Footer.js:
--------------------------------------------------------------------------------
1 | export function Footer(initialVnode) {
2 | return {
3 | view: function(vnode) {
4 | return m('footer.footer',
5 | m('div.credit',
6 | m('span', 'Created by '),
7 | m('a', {target: "_blank", href: "https://max-esnee.com"}, 'Max Esnée ↗')
8 | ),
9 | m('div.github',
10 | m('span', 'Source code available on '),
11 | m('a', {target: "_blank", href: "https://github.com/maxesnee/stack-and-justify"}, 'Github ↗')
12 | )
13 | )
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/js/components/Header.js:
--------------------------------------------------------------------------------
1 | import { AppState } from "../AppState.js";
2 | import { SVG } from "./SVG.js";
3 | import { FontInput } from "./FontInput.js";
4 | import { DarkModeButton } from "./DarkModeButton.js";
5 | import { OptionsMenu} from "./OptionsMenu.js";
6 | import { FeaturesMenu } from "./FeaturesMenu.js";
7 |
8 | export function Header(initialVnode) {
9 | return {
10 | view: function(vnode) {
11 | return m('header.header',
12 | m('h1.logo',
13 | m(SVG, {src: 'svg/logo.svg'}),
14 | m('span', 'Stack & Justify')
15 | ),
16 | m(FontInput),
17 | m(OptionsMenu),
18 | m(FeaturesMenu),
19 | m('div.header-btns',
20 | m(DarkModeButton),
21 | m('button.about-btn', {onclick: () => AppState.showAbout = !AppState.showAbout }, AppState.showAbout ? "❎" : "❓"),
22 | )
23 | )
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/js/components/IconSpinning.js:
--------------------------------------------------------------------------------
1 | export function IconSpinning(initialVnode) {
2 | return {
3 | view: function(vnode) {
4 | return m('svg.icon-spinning', {xmlns:'http://www.w3.org/2000/svg', viewBox:'0 0 1357 1358', width: 9.5, height: 9.5, fill:'currentColor'},
5 | m('path', {d: 'M677 215c60 0 109-49 109-107C786 48 737 0 677 0c-58 0-109 49-107 108 2 58 49 107 107 107Zm-403 976c60 0 109-50 107-107-2-61-47-107-107-107-58 0-107 48-107 107 0 58 49 107 107 107ZM107 786c59 0 108-47 108-107 0-58-49-107-108-107C47 572 0 621 0 679c0 60 47 107 107 107Zm572 572c58 0 107-49 107-108 0-58-49-107-107-107-60 0-107 49-107 107 0 59 48 108 107 108ZM274 382c57 0 107-47 107-107 0-59-47-108-107-107-59 1-108 48-107 107s49 107 107 107Zm809 808c57 0 107-43 107-107 0-60-48-107-107-107-61 0-107 47-107 107s46 107 107 107Zm167-404c59 0 107-48 107-107v-1c0-59-48-110-107-108-60 2-107 49-107 109 0 59 49 107 107 107Zm-168-404c60 0 107-47 107-107 0-61-47-106-107-107-59-1-107 46-107 107 0 59 49 107 107 107Z'})
6 | )
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/js/components/Line.js:
--------------------------------------------------------------------------------
1 | import { SizeInput } from "./SizeInput.js";
2 | import { FontSelect } from "./FontSelect.js";
3 | import { FilterSelect } from "./FilterSelect.js";
4 | import { CopyButton } from "./CopyButton.js";
5 | import { UpdateButton } from "./UpdateButton.js";
6 | import { DeleteButton } from "./DeleteButton.js";
7 | import { Layout } from "../Layout.js";
8 |
9 | export function Line(initialVnode) {
10 | return {
11 | view: function(vnode) {
12 | let line = vnode.attrs.line;
13 | return m('div', {class: 'specimen-line', id: line.id},
14 | m('div.line-left-col',
15 | m(SizeInput, {params: line}),
16 | m(FontSelect, {params: line})
17 | ),
18 | m('div.line-middle-col',
19 | m('div.text', {
20 | class: !line.outputFont.val.isLoading ? 'visible' : 'hidden',
21 | style: {
22 | whiteSpace: "nowrap",
23 | fontSize: line.outputSize.val+'px',
24 | width: Layout.width.get(),
25 | fontFamily: line.outputFont.val.fontFaceName,
26 | height: (line.outputSize.val * 1.2)+'px', // Get the line height
27 | fontFeatureSettings: line.outputFont.val.fontFeatureSettings.val
28 | }, }, line.text.val),
29 | !line.outputFont.val.isLoading && line.text.val === '' ? m('div.no-words-found', 'No words found ☹') : '',
30 | m('div.loading', {class: line.outputFont.val.isLoading ? 'visible' : 'hidden'},
31 | m('span', "Loading"),
32 | m('div.icon-spinning', "◌")
33 | )
34 | ),
35 | m('div.line-right-col',
36 | m(FilterSelect, {params: line}),
37 | m(CopyButton, {onclick: line.copyText}),
38 | m(UpdateButton, {onclick: line.update}),
39 | m(DeleteButton, {onclick: line.remove})
40 | )
41 | );
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/js/components/NewLineButton.js:
--------------------------------------------------------------------------------
1 | import { Layout } from '../Layout.js';
2 |
3 | export function NewLineButton(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.new-line', {onclick: () => Layout.addLine()},
7 | m('button.new-line-button', '⊕ Add new line')
8 | )
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/js/components/OptionsMenu.js:
--------------------------------------------------------------------------------
1 | import { Words } from "../Words.js";
2 | import { Layout } from "../Layout.js";
3 | import { Fonts } from "../Fonts.js";
4 |
5 | export function OptionsMenu(initialVnode) {
6 | // Menu status
7 | let open = false;
8 |
9 | let needsUpdate = false;
10 |
11 | // Temporarily holds the selected options before they are applies
12 | let options = {
13 | languages: Object.fromEntries(Words.data.languages.map(lang => [lang.name, lang.selected])),
14 | sources: Object.fromEntries(Words.data.sources.map(source => [source.name, source.selected]))
15 | };
16 |
17 | async function update(e) {
18 | e.preventDefault();
19 |
20 | // Update the selection of languages and dictionaries
21 | for (const language of Words.data.languages) {
22 | language.selected = options.languages[language.name];
23 | }
24 |
25 | for (const source of Words.data.sources) {
26 | source.selected = options.sources[source.name];
27 | }
28 |
29 | Fonts.updateAll();
30 |
31 | open = false;
32 | needsUpdate = false;
33 | }
34 |
35 | return {
36 | oncreate: function(vnode) {
37 | // Close the menu when the user clicks anywhere else
38 | document.addEventListener('click', (e) => {
39 | const menu = vnode.dom.querySelector('.menu');
40 | const btn = vnode.dom.querySelector('.menu-button');
41 |
42 | if (!menu.contains(e.target) && e.target !== btn && open) {
43 | open = false;
44 | m.redraw();
45 | }
46 | });
47 | },
48 | view: function(vnode) {
49 | return m('div.menu-container.options',
50 | m('button.menu-button', {onclick: () => { open = !open }}, "Options ▿"),
51 | m('form.menu', {style: {visibility: open ? 'visible' : 'hidden'}},
52 | m('fieldset.submenu',
53 | m('legend.submenu-header', 'Languages'),
54 | m('div.submenu-content',
55 | Words.data.languages.map(lang => {
56 | return m('div.checkbox',
57 | m('input', {
58 | name: 'languages',
59 | type: 'checkbox',
60 | id:lang.name,
61 | value: lang.name,
62 | checked: options.languages[lang.name],
63 | onclick: (e) => { options.languages[lang.name] = e.currentTarget.checked; needsUpdate = true; }
64 | }),
65 | m('label', {for: lang.name}, lang.label)
66 | )
67 | })
68 | )
69 | ),
70 | m('fieldset.submenu',
71 | m('legend.submenu-header', 'Sources'),
72 | m('div.submenu-content',
73 | Words.data.sources.map(source => {
74 | return m('div.checkbox',
75 | m('input', {
76 | name: 'sources',
77 | type: 'checkbox',
78 | id:source.name,
79 | value: source.name,
80 | checked: options.sources[source.name],
81 | onclick: (e) => { options.sources[source.name] = e.currentTarget.checked; needsUpdate = true; }
82 | }),
83 | m('label', {for: source.name}, source.label)
84 | )
85 | })
86 | )
87 | ),
88 | m('div.menu-update', {class: !needsUpdate ? "disabled" : ""},
89 | m('button.bold', {disabled: !needsUpdate, onclick: update},'↻ Update')
90 | )
91 | )
92 | )
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/js/components/SVG.js:
--------------------------------------------------------------------------------
1 | export function SVG(initialVnode) {
2 | return {
3 | oninit: function(vnode) {
4 | fetch(vnode.attrs.src)
5 | .then(response => response.text())
6 | .then(svgStr =>{
7 | const parser = new DOMParser();
8 | const svg = parser.parseFromString(svgStr, 'image/svg+xml').childNodes[0];
9 | vnode.dom.replaceWith(svg);
10 | })
11 |
12 | },
13 | view: function(vnode) {
14 | return m('div.svg');
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/js/components/SVGAnimation.js:
--------------------------------------------------------------------------------
1 | export function SVGAnimation(initialVnode) {
2 | function animate(svg, frames) {
3 | const fps = 12;
4 | let start;
5 | let lastFrameCount = 0;
6 | requestAnimationFrame(step);
7 |
8 | function step(timestamp) {
9 | if (start === undefined) start = timestamp;
10 | const elapsed = timestamp - start;
11 | const frameCount = Math.floor(elapsed / (1000/fps))%frames;
12 | if (frameCount !== lastFrameCount) {
13 | svg.children[lastFrameCount].setAttribute('display', 'none');
14 | svg.children[frameCount].setAttribute('display', 'block');
15 | }
16 |
17 | lastFrameCount = frameCount;
18 | requestAnimationFrame(step);
19 | }
20 | }
21 |
22 | return {
23 | oninit: function(vnode) {
24 | fetch(vnode.attrs.src)
25 | .then(response => response.text())
26 | .then(svgStr => {
27 | const parser = new DOMParser();
28 | const svg = parser.parseFromString(svgStr, 'image/svg+xml').childNodes[0];
29 | vnode.dom.replaceWith(svg);
30 | animate(svg, vnode.attrs.frames);
31 | })
32 | },
33 | view: function(vnode) {
34 | return m('div.svg-animation');
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/js/components/SizeInput.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 |
3 | export function SizeInput(initialVnode) {
4 | let isFocused = false;
5 | let editValue = "";
6 |
7 | function onfocus(e) {
8 | isFocused = true;
9 | editValue = e.target.value;
10 | }
11 |
12 | function oninput(e) {
13 | if (isFocused) {
14 | editValue = e.target.value;
15 | }
16 | }
17 |
18 | function onblur(e) {
19 | isFocused = false;
20 | }
21 |
22 | return {
23 | view: function(vnode) {
24 | return m('div.size-input', {class: Layout.sizeLocked.val ? "disabled" : ""},
25 | m('button', {
26 | onclick: () => { vnode.attrs.params.size.decrement() },
27 | disabled: Layout.sizeLocked.val
28 | }, '-'),
29 | m('input', {
30 | type: 'text',
31 | value: isFocused ? editValue : vnode.attrs.params.size.get(),
32 | onchange: (e) => {vnode.attrs.params.size.set(e.currentTarget.value)},
33 | disabled: Layout.sizeLocked.val,
34 | oninput,
35 | onfocus,
36 | onblur
37 | }),
38 | m('button', {
39 | onclick: () => { vnode.attrs.params.size.increment() },
40 | disabled: Layout.sizeLocked.val
41 | }, '+')
42 | )
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/js/components/SizeInputGlobal.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 | import { Tooltip } from "./Tooltip.js";
3 |
4 | export function SizeInputGlobal(initialVnode) {
5 | let isFocused = false;
6 | let editValue = "";
7 |
8 | function onfocus(e) {
9 | isFocused = true;
10 | editValue = e.target.value;
11 | }
12 |
13 | function oninput(e) {
14 | if (isFocused) {
15 | editValue = e.target.value;
16 | }
17 | }
18 |
19 | function onblur(e) {
20 | isFocused = false;
21 | }
22 |
23 | return {
24 | view: function(vnode) {
25 | return m('div.size-input.size-input-global',
26 | m('div.size-input-wrapper', {class: !Layout.sizeLocked.val ? "disabled" : ""},
27 | m('button', {
28 | onclick: () => { Layout.size.decrement() },
29 | disabled: !Layout.sizeLocked.val
30 | }, '-'),
31 | m('input', {
32 | type: 'text',
33 | value: isFocused ? editValue : Layout.size.get(),
34 | onchange: (e) => {Layout.size.set(e.currentTarget.value)},
35 | disabled: !Layout.sizeLocked.val,
36 | oninput,
37 | onfocus,
38 | onblur
39 | }),
40 | m('button', {
41 | onclick: () => { Layout.size.increment() },
42 | disabled: !Layout.sizeLocked.val
43 | }, '+')
44 | ),
45 | m('div.size-input-lock',
46 | m('button', {
47 | onclick: () => {Layout.sizeLocked.val = !Layout.sizeLocked.val}
48 | }, `${Layout.sizeLocked.val ? '🔒' : '🔓'}`),
49 | m(Tooltip, {label: 'Apply to all lines'})
50 | )
51 | )
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/js/components/Specimen.js:
--------------------------------------------------------------------------------
1 | import { AppState } from "../AppState.js";
2 | import { Layout } from "../Layout.js";
3 | import { Fonts } from "../Fonts.js";
4 | import { Line } from "./Line.js";
5 | import { SizeInputGlobal } from "./SizeInputGlobal.js";
6 | import { FontSelectGlobal } from "./FontSelectGlobal.js";
7 | import { WidthInput } from "./WidthInput.js";
8 | import { FilterSelectGlobal } from "./FilterSelectGlobal.js";
9 | import { CopyButtonGlobal } from "./CopyButtonGlobal.js";
10 | import { UpdateButtonGlobal } from "./UpdateButtonGlobal.js";
11 | import { NewLineButton } from "./NewLineButton.js";
12 | import { DeleteButtonGlobal } from "./DeleteButtonGlobal.js";
13 |
14 |
15 | export function Specimen(initialVnode) {
16 | let isDragging = false;
17 | let draggedEl = null;
18 | let draggedClone = null;
19 | let dragStartPosX;
20 | let dragStartPosY;
21 |
22 | function onmousedown(e) {
23 | const target = e.target;
24 |
25 | // Don't prevent the text itself from being selected
26 | if (target.classList.contains('text')) return;
27 |
28 | // Only target the line itself or the immediate children
29 | // to prevent from triggering dragging when interacting with inputs
30 | if (!target.classList.contains('specimen-line') &&
31 | !target.parentElement.classList.contains('specimen-line')) {
32 | return;
33 | }
34 |
35 | // Mark the line being dragged
36 | draggedEl = target.closest('.specimen-line');
37 |
38 | // Prevent triggering text selection while dragging
39 | draggedEl.parentElement.style.userSelect = 'none';
40 | draggedEl.parentElement.style.webkitUserSelect = 'none';
41 |
42 | // Deselect all text
43 | window.getSelection().removeAllRanges()
44 |
45 | // Create a clone of the line and display it
46 | draggedClone = createClone(draggedEl);
47 | draggedEl.insertAdjacentElement('beforebegin', draggedClone);
48 |
49 | // Hide the line being dragged
50 | draggedEl.classList.add('dragged');
51 |
52 | // Get the mouse position
53 | dragStartPosX = e.clientX;
54 | dragStartPosY = e.clientY;
55 |
56 | isDragging = true;
57 |
58 | // console.log(`started dragging: ${target.className}`);
59 | }
60 |
61 | function onmousemove(e) {
62 | if (!isDragging) return;
63 |
64 | const dragOffsetX = e.clientX - dragStartPosX;
65 | const dragOffsetY = e.clientY - dragStartPosY;
66 |
67 | // The clone follows the mouse while dragging
68 | draggedClone.style.transform = `translate(${dragOffsetX}px, ${dragOffsetY}px)`;
69 |
70 | // Get the drop target
71 | const lineEls = document.querySelectorAll('.specimen-line');
72 | lineEls.forEach(lineEl => {
73 | if (lineEl === draggedEl) return;
74 |
75 | const rect = lineEl.getBoundingClientRect();
76 | if (e.clientY > rect.top && e.clientY <= rect.bottom) {
77 | // Get the line and move to the new index
78 | const line = Layout.getLine(draggedEl.id);
79 | const targetIndex = Layout.indexOf(lineEl.id);
80 |
81 | Layout.moveLine(line, targetIndex);
82 | m.redraw();
83 | }
84 | });
85 |
86 | // console.log('dragging');
87 | }
88 |
89 | function onmouseup(e) {
90 | if (!isDragging) return;
91 |
92 | draggedEl.classList.remove('dragged');
93 | draggedEl.parentElement.style.userSelect = '';
94 | draggedEl.parentElement.style.webkitUserSelect = '';
95 | draggedEl = null;
96 |
97 | draggedClone.remove();
98 | draggedClone = null;
99 |
100 | isDragging = false;
101 |
102 | // console.log('stopped dragging');
103 | }
104 |
105 |
106 | return {
107 | oncreate: function(vnode) {
108 | vnode.dom.querySelector('.specimen-body').addEventListener('mousedown', onmousedown);
109 | window.addEventListener('mousemove', onmousemove);
110 | window.addEventListener('mouseup', onmouseup);
111 | },
112 | view: function(vnode) {
113 | return m('div', {class: 'specimen', style: {display: AppState.showAbout ? 'none' : ''}},
114 | m('header.specimen-header',
115 | m('div.line-left-col',
116 | m(SizeInputGlobal),
117 | Fonts.length ? m(FontSelectGlobal) : ''
118 | ),
119 | m('div.line-middle-col',
120 | m(WidthInput)
121 | ),
122 | m('div.line-right-col',
123 | m(FilterSelectGlobal),
124 | m(CopyButtonGlobal, {onclick: Layout.copyText}),
125 | m(UpdateButtonGlobal, {onclick: Layout.update}),
126 | m(DeleteButtonGlobal, {onclick: Layout.clear})
127 | ),
128 | ),
129 | m('div.specimen-body',
130 | Layout.lines.map((line) => m(Line, {line, key:line.id})),
131 | m(NewLineButton)
132 | )
133 | )
134 | }
135 | }
136 | }
137 |
138 | function createClone(target) {
139 | const rect = target.getBoundingClientRect();
140 | const clone = target.cloneNode(true);
141 | const cloneChildEls = clone.children;
142 | const targetChildEls = target.children;
143 | const cloneSelectEls = clone.querySelectorAll('select');
144 | const targetSelectEls = target.querySelectorAll('select');
145 |
146 | target.style.position = "relative";
147 | clone.classList.add('drag-clone');
148 |
149 | // The clone is fixed, so it loses the grid of the target element
150 | // to keep it visually identical, we have to position everything absolutely
151 | clone.style.position = 'fixed';
152 | clone.style.width = rect.width + 'px';
153 | clone.style.height = rect.height + 'px';
154 | clone.style.left = rect.left + 'px';
155 | clone.style.top = rect.top + 'px';
156 |
157 | for (let i = 0; i < cloneChildEls.length; i++) {
158 | cloneChildEls[i].style.position = 'absolute';
159 | cloneChildEls[i].style.left = targetChildEls[i].offsetLeft + 'px';
160 | cloneChildEls[i].style.width = targetChildEls[i].offsetWidth + 'px';
161 | cloneChildEls[i].style.top = targetChildEls[i].offsetTop + 'px';
162 | cloneChildEls[i].style.height = targetChildEls[i].offsetHeight + 'px';
163 | }
164 |
165 | // The clone doesn't inherit the selected options
166 | for (let i = 0; i < cloneSelectEls.length; i++) {
167 | cloneSelectEls[i].value = targetSelectEls[i].value;
168 | }
169 |
170 | return clone;
171 | }
--------------------------------------------------------------------------------
/js/components/SplashScreen.js:
--------------------------------------------------------------------------------
1 | import { SVGAnimation } from "./SVGAnimation.js";
2 |
3 | export function SplashScreen(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.splash-screen',
7 | m('div.splash-screen-text',
8 | m(SVGAnimation, {src: 'svg/font-files-animation.svg', frames: 18}),
9 | m('p.t-big', 'To start, drop one or more font files anywhere on the window.'),
10 | m('p.splash-screen-notice', 'Your font files are not uploaded, they remain stored locally in your browser.')
11 | )
12 | )
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/js/components/Tooltip.js:
--------------------------------------------------------------------------------
1 | export function Tooltip(initialVnode) {
2 |
3 | function onclick(vnode) {
4 | if (vnode.attrs.activeLabel) {
5 | vnode.dom.textContent = vnode.attrs.activeLabel;
6 | positionTooltip(vnode);
7 | }
8 | }
9 |
10 | function onmouseleave(vnode) {
11 | if (vnode.attrs.activeLabel) {
12 | vnode.dom.textContent = vnode.attrs.label;
13 | positionTooltip(vnode);
14 | }
15 | }
16 |
17 | function positionTooltip(vnode) {
18 | const rect = vnode.dom.getBoundingClientRect();
19 | const parentRect = vnode.dom.parentElement.getBoundingClientRect();
20 | const padding = parseFloat(getComputedStyle(document.body).getPropertyValue('padding-right'))/2;
21 | let pos = -rect.width/2 + parentRect.width/2;
22 |
23 | // if tooltip is outside the viewport
24 | if (rect.right + pos + padding > document.body.clientWidth) {
25 | pos -= (rect.right + pos + padding) - document.body.clientWidth;
26 | }
27 |
28 | vnode.dom.style.transform = `translateX(${pos}px)`;
29 | }
30 |
31 | return {
32 | oncreate: function(vnode) {
33 | positionTooltip(vnode);
34 | vnode.dom.parentElement.addEventListener('click', () => onclick(vnode));
35 | vnode.dom.parentElement.addEventListener('mouseleave', () => onmouseleave(vnode));
36 | },
37 | view: function(vnode) {
38 | return m('div.tooltip', vnode.attrs.label);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/js/components/UpdateButton.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function UpdateButton(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.update-button',
7 | m('button', {onclick: vnode.attrs.onclick },'↻'),
8 | m(Tooltip, {label: 'Refresh line'})
9 | )
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/js/components/UpdateButtonGlobal.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from './Tooltip.js';
2 |
3 | export function UpdateButtonGlobal(initialVnode) {
4 | return {
5 | view: function(vnode) {
6 | return m('div.update-button',
7 | m('button', {onclick: vnode.attrs.onclick },'↻'),
8 | m(Tooltip, {label: 'Refresh all lines'})
9 | )
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/js/components/WidthInput.js:
--------------------------------------------------------------------------------
1 | import { Layout } from "../Layout.js";
2 |
3 | export function WidthInput(initialVnode) {
4 | let isDragging = false;
5 | let startFromRight = null;
6 | let startWidth = Layout.width.getIn('px');
7 | let startX = 0;
8 | let dX = 0;
9 | let isFocused = false;
10 | let editValue = "";
11 |
12 | document.body.onmousemove = onmousemove;
13 | document.body.onmouseup = onmouseup;
14 |
15 | function onmousedown(e) {
16 | startFromRight = e.currentTarget.classList.contains('right');
17 | isDragging = true;
18 | startX = e.clientX;
19 | startWidth = Layout.width.getIn('px');
20 | }
21 |
22 | function onmousemove(e) {
23 | if (isDragging) {
24 | dX = e.clientX - startX;
25 |
26 | // Invert delta if dragging started from left side
27 | dX = startFromRight ? dX : -dX;
28 |
29 | // Width is distributed on both side, so this allow
30 | // the resizing to be in sync with cursor visually
31 | dX = dX*2;
32 |
33 | // Prevent from getting negative width
34 | dX = dX > -startWidth ? dX : -startWidth;
35 |
36 | // Move the label if the input is too small
37 | const label = document.querySelector('.width-input input');
38 |
39 | if (label.offsetWidth > startWidth + dX) {
40 | label.classList.add('small');
41 | } else {
42 | label.classList.remove('small');
43 | }
44 |
45 | Layout.width.setIn('px', startWidth + dX);
46 | m.redraw();
47 | }
48 | }
49 |
50 | function onmouseup(e) {
51 | isDragging = false;
52 | }
53 |
54 | function onfocus(e) {
55 | isFocused = true;
56 | editValue = e.target.value;
57 | }
58 |
59 | function oninput(e) {
60 | if (isFocused) {
61 | editValue = e.target.value;
62 | }
63 | }
64 |
65 | function onblur(e) {
66 | isFocused = false;
67 | }
68 |
69 | return {
70 | view: function(vnode) {
71 | return m('div.width-input', {style: { width: Layout.width.get()}},
72 | m('div.width-input-handle.left', {onmousedown}),
73 | m('span.width-input-line'),
74 | m('input', {
75 | type: 'text',
76 | value: isFocused ? editValue : Layout.width.get(),
77 | onchange: (e) => {Layout.width.set(e.currentTarget.value)},
78 | oninput,
79 | onfocus,
80 | onblur
81 | }),
82 | m('span.width-input-line'),
83 | m('div.width-input-handle.right', {onmousedown})
84 | )
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/js/harfbuzzjs/hb.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxesnee/stack-and-justify/fb54fe697996d5d5592ad5d98e0d632213b3417d/js/harfbuzzjs/hb.wasm
--------------------------------------------------------------------------------
/js/harfbuzzjs/hbjs.js:
--------------------------------------------------------------------------------
1 | export function hbjs(instance) {
2 | var exports = instance.exports;
3 | var heapu8 = new Uint8Array(exports.memory.buffer);
4 | var heapu32 = new Uint32Array(exports.memory.buffer);
5 | var heapi32 = new Int32Array(exports.memory.buffer);
6 | var heapf32 = new Float32Array(exports.memory.buffer);
7 |
8 | var HB_MEMORY_MODE_WRITABLE = 2;
9 | var HB_SET_VALUE_INVALID = -1;
10 | var HB_BUFFER_CONTENT_TYPE_GLYPHS = 2;
11 | var DONT_STOP = 0;
12 | var GSUB_PHASE = 1;
13 | var GPOS_PHASE = 2;
14 |
15 | function hb_tag(s) {
16 | return (
17 | (s.charCodeAt(0) & 0xFF) << 24 |
18 | (s.charCodeAt(1) & 0xFF) << 16 |
19 | (s.charCodeAt(2) & 0xFF) << 8 |
20 | (s.charCodeAt(3) & 0xFF) << 0
21 | );
22 | }
23 |
24 | var HB_BUFFER_SERIALIZE_FORMAT_JSON = hb_tag('JSON');
25 | var HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES = 4;
26 |
27 | function _hb_untag(tag) {
28 | return [
29 | String.fromCharCode((tag >> 24) & 0xFF),
30 | String.fromCharCode((tag >> 16) & 0xFF),
31 | String.fromCharCode((tag >> 8) & 0xFF),
32 | String.fromCharCode((tag >> 0) & 0xFF)
33 | ].join('');
34 | }
35 |
36 | function _buffer_flag(s) {
37 | if (s == "BOT") { return 0x1; }
38 | if (s == "EOT") { return 0x2; }
39 | if (s == "PRESERVE_DEFAULT_IGNORABLES") { return 0x4; }
40 | if (s == "REMOVE_DEFAULT_IGNORABLES") { return 0x8; }
41 | if (s == "DO_NOT_INSERT_DOTTED_CIRCLE") { return 0x10; }
42 | if (s == "PRODUCE_UNSAFE_TO_CONCAT") { return 0x40; }
43 | return 0x0;
44 | }
45 |
46 | /**
47 | * Create an object representing a Harfbuzz blob.
48 | * @param {string} blob A blob of binary data (usually the contents of a font file).
49 | **/
50 | function createBlob(blob) {
51 | var blobPtr = exports.malloc(blob.byteLength);
52 | heapu8.set(new Uint8Array(blob), blobPtr);
53 | var ptr = exports.hb_blob_create(blobPtr, blob.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr);
54 | return {
55 | ptr: ptr,
56 | /**
57 | * Free the object.
58 | */
59 | destroy: function () { exports.hb_blob_destroy(ptr); }
60 | };
61 | }
62 |
63 | /**
64 | * Return the typed array of HarfBuzz set contents.
65 | * @template {typeof Uint8Array | typeof Uint32Array | typeof Int32Array | typeof Float32Array} T
66 | * @param {number} setPtr Pointer of set
67 | * @param {T} arrayClass Typed array class
68 | * @returns {InstanceType} Typed array instance
69 | */
70 | function typedArrayFromSet(setPtr, arrayClass) {
71 | let heap = heapu8;
72 | if (arrayClass === Uint32Array) {
73 | heap = heapu32;
74 | } else if (arrayClass === Int32Array) {
75 | heap = heapi32;
76 | } else if (arrayClass === Float32Array) {
77 | heap = heapf32;
78 | }
79 | const bytesPerElment = arrayClass.BYTES_PER_ELEMENT;
80 | const setCount = exports.hb_set_get_population(setPtr);
81 | const arrayPtr = exports.malloc(
82 | setCount * bytesPerElment,
83 | );
84 | const arrayOffset = arrayPtr / bytesPerElment;
85 | const array = heap.subarray(
86 | arrayOffset,
87 | arrayOffset + setCount,
88 | );
89 | heap.set(array, arrayOffset);
90 | exports.hb_set_next_many(
91 | setPtr,
92 | HB_SET_VALUE_INVALID,
93 | arrayPtr,
94 | setCount,
95 | );
96 | return array;
97 | }
98 |
99 | /**
100 | * Create an object representing a Harfbuzz face.
101 | * @param {object} blob An object returned from `createBlob`.
102 | * @param {number} index The index of the font in the blob. (0 for most files,
103 | * or a 0-indexed font number if the `blob` came form a TTC/OTC file.)
104 | **/
105 | function createFace(blob, index) {
106 | var ptr = exports.hb_face_create(blob.ptr, index);
107 | const upem = exports.hb_face_get_upem(ptr);
108 | return {
109 | ptr: ptr,
110 | upem,
111 | /**
112 | * Return the binary contents of an OpenType table.
113 | * @param {string} table Table name
114 | */
115 | reference_table: function(table) {
116 | var blob = exports.hb_face_reference_table(ptr, hb_tag(table));
117 | var length = exports.hb_blob_get_length(blob);
118 | if (!length) { return; }
119 | var blobptr = exports.hb_blob_get_data(blob, null);
120 | var table_string = heapu8.subarray(blobptr, blobptr+length);
121 | return table_string;
122 | },
123 | /**
124 | * Return variation axis infos
125 | */
126 | getAxisInfos: function() {
127 | var axis = exports.malloc(64 * 32);
128 | var c = exports.malloc(4);
129 | heapu32[c / 4] = 64;
130 | exports.hb_ot_var_get_axis_infos(ptr, 0, c, axis);
131 | var result = {};
132 | Array.from({ length: heapu32[c / 4] }).forEach(function (_, i) {
133 | result[_hb_untag(heapu32[axis / 4 + i * 8 + 1])] = {
134 | min: heapf32[axis / 4 + i * 8 + 4],
135 | default: heapf32[axis / 4 + i * 8 + 5],
136 | max: heapf32[axis / 4 + i * 8 + 6]
137 | };
138 | });
139 | exports.free(c);
140 | exports.free(axis);
141 | return result;
142 | },
143 | /**
144 | * Return unicodes the face supports
145 | */
146 | collectUnicodes: function() {
147 | var unicodeSetPtr = exports.hb_set_create();
148 | exports.hb_face_collect_unicodes(ptr, unicodeSetPtr);
149 | var result = typedArrayFromSet(unicodeSetPtr, Uint32Array);
150 | exports.hb_set_destroy(unicodeSetPtr);
151 | return result;
152 | },
153 | /**
154 | * Free the object.
155 | */
156 | destroy: function () {
157 | exports.hb_face_destroy(ptr);
158 | },
159 | };
160 | }
161 |
162 | var pathBuffer = "";
163 |
164 | var nameBufferSize = 256; // should be enough for most glyphs
165 | var nameBuffer = exports.malloc(nameBufferSize); // permanently allocated
166 |
167 | /**
168 | * Create an object representing a Harfbuzz font.
169 | * @param {object} blob An object returned from `createFace`.
170 | **/
171 | function createFont(face) {
172 | var ptr = exports.hb_font_create(face.ptr);
173 | var drawFuncsPtr = null;
174 |
175 | /**
176 | * Return a glyph as an SVG path string.
177 | * @param {number} glyphId ID of the requested glyph in the font.
178 | **/
179 | function glyphToPath(glyphId) {
180 | if (!drawFuncsPtr) {
181 | var moveTo = function (dfuncs, draw_data, draw_state, to_x, to_y, user_data) {
182 | pathBuffer += `M${to_x},${to_y}`;
183 | }
184 | var lineTo = function (dfuncs, draw_data, draw_state, to_x, to_y, user_data) {
185 | pathBuffer += `L${to_x},${to_y}`;
186 | }
187 | var cubicTo = function (dfuncs, draw_data, draw_state, c1_x, c1_y, c2_x, c2_y, to_x, to_y, user_data) {
188 | pathBuffer += `C${c1_x},${c1_y} ${c2_x},${c2_y} ${to_x},${to_y}`;
189 | }
190 | var quadTo = function (dfuncs, draw_data, draw_state, c_x, c_y, to_x, to_y, user_data) {
191 | pathBuffer += `Q${c_x},${c_y} ${to_x},${to_y}`;
192 | }
193 | var closePath = function (dfuncs, draw_data, draw_state, user_data) {
194 | pathBuffer += 'Z';
195 | }
196 |
197 | var moveToPtr = addFunction(moveTo, 'viiiffi');
198 | var lineToPtr = addFunction(lineTo, 'viiiffi');
199 | var cubicToPtr = addFunction(cubicTo, 'viiiffffffi');
200 | var quadToPtr = addFunction(quadTo, 'viiiffffi');
201 | var closePathPtr = addFunction(closePath, 'viiii');
202 | drawFuncsPtr = exports.hb_draw_funcs_create();
203 | exports.hb_draw_funcs_set_move_to_func(drawFuncsPtr, moveToPtr, 0, 0);
204 | exports.hb_draw_funcs_set_line_to_func(drawFuncsPtr, lineToPtr, 0, 0);
205 | exports.hb_draw_funcs_set_cubic_to_func(drawFuncsPtr, cubicToPtr, 0, 0);
206 | exports.hb_draw_funcs_set_quadratic_to_func(drawFuncsPtr, quadToPtr, 0, 0);
207 | exports.hb_draw_funcs_set_close_path_func(drawFuncsPtr, closePathPtr, 0, 0);
208 | }
209 |
210 | pathBuffer = "";
211 | exports.hb_font_draw_glyph(ptr, glyphId, drawFuncsPtr, 0);
212 | return pathBuffer;
213 | }
214 |
215 | /**
216 | * Return glyph name.
217 | * @param {number} glyphId ID of the requested glyph in the font.
218 | **/
219 | function glyphName(glyphId) {
220 | exports.hb_font_glyph_to_string(
221 | ptr,
222 | glyphId,
223 | nameBuffer,
224 | nameBufferSize
225 | );
226 | var array = heapu8.subarray(nameBuffer, nameBuffer + nameBufferSize);
227 | return utf8Decoder.decode(array.slice(0, array.indexOf(0)));
228 | }
229 |
230 | return {
231 | ptr: ptr,
232 | glyphName: glyphName,
233 | glyphToPath: glyphToPath,
234 | /**
235 | * Return a glyph as a JSON path string
236 | * based on format described on https://svgwg.org/specs/paths/#InterfaceSVGPathSegment
237 | * @param {number} glyphId ID of the requested glyph in the font.
238 | **/
239 | glyphToJson: function (glyphId) {
240 | var path = glyphToPath(glyphId);
241 | return path.replace(/([MLQCZ])/g, '|$1 ').split('|').filter(function (x) { return x.length; }).map(function (x) {
242 | var row = x.split(/[ ,]/g);
243 | return { type: row[0], values: row.slice(1).filter(function (x) { return x.length; }).map(function (x) { return +x; }) };
244 | });
245 | },
246 | /**
247 | * Set the font's scale factor, affecting the position values returned from
248 | * shaping.
249 | * @param {number} xScale Units to scale in the X dimension.
250 | * @param {number} yScale Units to scale in the Y dimension.
251 | **/
252 | setScale: function (xScale, yScale) {
253 | exports.hb_font_set_scale(ptr, xScale, yScale);
254 | },
255 | /**
256 | * Set the font's variations.
257 | * @param {object} variations Dictionary of variations to set
258 | **/
259 | setVariations: function (variations) {
260 | var entries = Object.entries(variations);
261 | var vars = exports.malloc(8 * entries.length);
262 | entries.forEach(function (entry, i) {
263 | heapu32[vars / 4 + i * 2 + 0] = hb_tag(entry[0]);
264 | heapf32[vars / 4 + i * 2 + 1] = entry[1];
265 | });
266 | exports.hb_font_set_variations(ptr, vars, entries.length);
267 | exports.free(vars);
268 | },
269 | /**
270 | * Free the object.
271 | */
272 | destroy: function () { exports.hb_font_destroy(ptr); }
273 | };
274 | }
275 |
276 | /**
277 | * Use when you know the input range should be ASCII.
278 | * Faster than encoding to UTF-8
279 | **/
280 | function createAsciiString(text) {
281 | var ptr = exports.malloc(text.length + 1);
282 | for (let i = 0; i < text.length; ++i) {
283 | const char = text.charCodeAt(i);
284 | if (char > 127) throw new Error('Expected ASCII text');
285 | heapu8[ptr + i] = char;
286 | }
287 | heapu8[ptr + text.length] = 0;
288 | return {
289 | ptr: ptr,
290 | length: text.length,
291 | free: function () { exports.free(ptr); }
292 | };
293 | }
294 |
295 | function createJsString(text) {
296 | const ptr = exports.malloc(text.length * 2);
297 | const words = new Uint16Array(exports.memory.buffer, ptr, text.length);
298 | for (let i = 0; i < words.length; ++i) words[i] = text.charCodeAt(i);
299 | return {
300 | ptr: ptr,
301 | length: words.length,
302 | free: function () { exports.free(ptr); }
303 | };
304 | }
305 |
306 | /**
307 | * Create an object representing a Harfbuzz buffer.
308 | **/
309 | function createBuffer() {
310 | var ptr = exports.hb_buffer_create();
311 | return {
312 | ptr: ptr,
313 | /**
314 | * Add text to the buffer.
315 | * @param {string} text Text to be added to the buffer.
316 | **/
317 | addText: function (text) {
318 | const str = createJsString(text);
319 | exports.hb_buffer_add_utf16(ptr, str.ptr, str.length, 0, str.length);
320 | str.free();
321 | },
322 | /**
323 | * Set buffer script, language and direction.
324 | *
325 | * This needs to be done before shaping.
326 | **/
327 | guessSegmentProperties: function () {
328 | return exports.hb_buffer_guess_segment_properties(ptr);
329 | },
330 | /**
331 | * Set buffer direction explicitly.
332 | * @param {string} direction: One of "ltr", "rtl", "ttb" or "btt"
333 | */
334 | setDirection: function (dir) {
335 | exports.hb_buffer_set_direction(ptr, {
336 | ltr: 4,
337 | rtl: 5,
338 | ttb: 6,
339 | btt: 7
340 | }[dir] || 0);
341 | },
342 | /**
343 | * Set buffer flags explicitly.
344 | * @param {string[]} flags: A list of strings which may be either:
345 | * "BOT"
346 | * "EOT"
347 | * "PRESERVE_DEFAULT_IGNORABLES"
348 | * "REMOVE_DEFAULT_IGNORABLES"
349 | * "DO_NOT_INSERT_DOTTED_CIRCLE"
350 | * "PRODUCE_UNSAFE_TO_CONCAT"
351 | */
352 | setFlags: function (flags) {
353 | var flagValue = 0
354 | flags.forEach(function (s) {
355 | flagValue |= _buffer_flag(s);
356 | })
357 |
358 | exports.hb_buffer_set_flags(ptr,flagValue);
359 | },
360 | /**
361 | * Set buffer language explicitly.
362 | * @param {string} language: The buffer language
363 | */
364 | setLanguage: function (language) {
365 | var str = createAsciiString(language);
366 | exports.hb_buffer_set_language(ptr, exports.hb_language_from_string(str.ptr,-1));
367 | str.free();
368 | },
369 | /**
370 | * Set buffer script explicitly.
371 | * @param {string} script: The buffer script
372 | */
373 | setScript: function (script) {
374 | var str = createAsciiString(script);
375 | exports.hb_buffer_set_script(ptr, exports.hb_script_from_string(str.ptr,-1));
376 | str.free();
377 | },
378 |
379 | /**
380 | * Set the Harfbuzz clustering level.
381 | *
382 | * Affects the cluster values returned from shaping.
383 | * @param {number} level: Clustering level. See the Harfbuzz manual chapter
384 | * on Clusters.
385 | **/
386 | setClusterLevel: function (level) {
387 | exports.hb_buffer_set_cluster_level(ptr, level)
388 | },
389 | /**
390 | * Return the buffer contents as a JSON object.
391 | *
392 | * After shaping, this function will return an array of glyph information
393 | * objects. Each object will have the following attributes:
394 | *
395 | * - g: The glyph ID
396 | * - cl: The cluster ID
397 | * - ax: Advance width (width to advance after this glyph is painted)
398 | * - ay: Advance height (height to advance after this glyph is painted)
399 | * - dx: X displacement (adjustment in X dimension when painting this glyph)
400 | * - dy: Y displacement (adjustment in Y dimension when painting this glyph)
401 | * - flags: Glyph flags like `HB_GLYPH_FLAG_UNSAFE_TO_BREAK` (0x1)
402 | **/
403 | json: function () {
404 | var length = exports.hb_buffer_get_length(ptr);
405 | var result = [];
406 | var infosPtr = exports.hb_buffer_get_glyph_infos(ptr, 0);
407 | var infosPtr32 = infosPtr / 4;
408 | var positionsPtr32 = exports.hb_buffer_get_glyph_positions(ptr, 0) / 4;
409 | var infos = heapu32.subarray(infosPtr32, infosPtr32 + 5 * length);
410 | var positions = heapi32.subarray(positionsPtr32, positionsPtr32 + 5 * length);
411 | for (var i = 0; i < length; ++i) {
412 | result.push({
413 | g: infos[i * 5 + 0],
414 | cl: infos[i * 5 + 2],
415 | ax: positions[i * 5 + 0],
416 | ay: positions[i * 5 + 1],
417 | dx: positions[i * 5 + 2],
418 | dy: positions[i * 5 + 3],
419 | flags: exports.hb_glyph_info_get_glyph_flags(infosPtr + i * 20)
420 | });
421 | }
422 | return result;
423 | },
424 | /**
425 | * Free the object.
426 | */
427 | destroy: function () { exports.hb_buffer_destroy(ptr); }
428 | };
429 | }
430 |
431 | /**
432 | * Shape a buffer with a given font.
433 | *
434 | * This returns nothing, but modifies the buffer.
435 | *
436 | * @param {object} font: A font returned from `createFont`
437 | * @param {object} buffer: A buffer returned from `createBuffer` and suitably
438 | * prepared.
439 | * @param {object} features: A string of comma-separated OpenType features to apply.
440 | */
441 | function shape(font, buffer, features) {
442 | var featuresPtr = 0;
443 | var featuresLen = 0;
444 | if (features) {
445 | features = features.split(",");
446 | featuresPtr = exports.malloc(16 * features.length);
447 | features.forEach(function (feature, i) {
448 | var str = createAsciiString(feature);
449 | if (exports.hb_feature_from_string(str.ptr, -1, featuresPtr + featuresLen * 16))
450 | featuresLen++;
451 | str.free();
452 | });
453 | }
454 |
455 | exports.hb_shape(font.ptr, buffer.ptr, featuresPtr, featuresLen);
456 | if (featuresPtr)
457 | exports.free(featuresPtr);
458 | }
459 |
460 | /**
461 | * Shape a buffer with a given font, returning a JSON trace of the shaping process.
462 | *
463 | * This function supports "partial shaping", where the shaping process is
464 | * terminated after a given lookup ID is reached. If the user requests the function
465 | * to terminate shaping after an ID in the GSUB phase, GPOS table lookups will be
466 | * processed as normal.
467 | *
468 | * @param {object} font: A font returned from `createFont`
469 | * @param {object} buffer: A buffer returned from `createBuffer` and suitably
470 | * prepared.
471 | * @param {object} features: A string of comma-separated OpenType features to apply.
472 | * @param {number} stop_at: A lookup ID at which to terminate shaping.
473 | * @param {number} stop_phase: Either 0 (don't terminate shaping), 1 (`stop_at`
474 | refers to a lookup ID in the GSUB table), 2 (`stop_at` refers to a lookup
475 | ID in the GPOS table).
476 | */
477 | function shapeWithTrace(font, buffer, features, stop_at, stop_phase) {
478 | var trace = [];
479 | var currentPhase = DONT_STOP;
480 | var stopping = false;
481 | var failure = false;
482 |
483 | var traceBufLen = 1024 * 1024;
484 | var traceBufPtr = exports.malloc(traceBufLen);
485 |
486 | var traceFunc = function (bufferPtr, fontPtr, messagePtr, user_data) {
487 | var message = utf8Decoder.decode(heapu8.subarray(messagePtr, heapu8.indexOf(0, messagePtr)));
488 | if (message.startsWith("start table GSUB"))
489 | currentPhase = GSUB_PHASE;
490 | else if (message.startsWith("start table GPOS"))
491 | currentPhase = GPOS_PHASE;
492 |
493 | if (currentPhase != stop_phase)
494 | stopping = false;
495 |
496 | if (failure)
497 | return 1;
498 |
499 | if (stop_phase != DONT_STOP && currentPhase == stop_phase && message.startsWith("end lookup " + stop_at))
500 | stopping = true;
501 |
502 | if (stopping)
503 | return 0;
504 |
505 | exports.hb_buffer_serialize_glyphs(
506 | bufferPtr,
507 | 0, exports.hb_buffer_get_length(bufferPtr),
508 | traceBufPtr, traceBufLen, 0,
509 | fontPtr,
510 | HB_BUFFER_SERIALIZE_FORMAT_JSON,
511 | HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES);
512 |
513 | trace.push({
514 | m: message,
515 | t: JSON.parse(utf8Decoder.decode(heapu8.subarray(traceBufPtr, heapu8.indexOf(0, traceBufPtr)))),
516 | glyphs: exports.hb_buffer_get_content_type(bufferPtr) == HB_BUFFER_CONTENT_TYPE_GLYPHS,
517 | });
518 |
519 | return 1;
520 | }
521 |
522 | var traceFuncPtr = addFunction(traceFunc, 'iiiii');
523 | exports.hb_buffer_set_message_func(buffer.ptr, traceFuncPtr, 0, 0);
524 | shape(font, buffer, features, 0);
525 | exports.free(traceBufPtr);
526 |
527 | return trace;
528 | }
529 |
530 | return {
531 | createBlob: createBlob,
532 | createFace: createFace,
533 | createFont: createFont,
534 | createBuffer: createBuffer,
535 | shape: shape,
536 | shapeWithTrace: shapeWithTrace
537 | };
538 | };
--------------------------------------------------------------------------------
/js/miniotparser/MiniOTParser.js:
--------------------------------------------------------------------------------
1 | /* Mini OpenType Parser
2 | *
3 | * This is not a proper OpenType parser, please don't use it.
4 | * If you want an actual OpenType JS library, check opentype.js or Typr.js
5 | *
6 | * I made this because I only needed a handful of entries from the 'name', 'OS/2'
7 | * and 'GSUB' table and parsing the entire font seemed wasteful and was actually slow
8 | * with opentype.js. Typr.js is faster but doesn't support the 'GSUB' table.
9 | *
10 | */
11 |
12 | const nameTableNames = [
13 | 'copyright',
14 | 'fontFamily',
15 | 'fontSubfamily',
16 | 'uniqueID',
17 | 'fullName',
18 | 'version',
19 | 'postScriptName',
20 | 'trademark',
21 | 'manufacturer',
22 | 'designer',
23 | 'description',
24 | 'manufacturerURL',
25 | 'designerURL',
26 | 'license',
27 | 'licenseURL',
28 | 'reserved',
29 | 'typoFamily',
30 | 'typoSubfamily',
31 | 'compatibleFullName',
32 | 'sampleText',
33 | 'postScriptFindFontName',
34 | 'wwsFamily',
35 | 'wwsSubfamily'
36 | ];
37 |
38 | // List of OpenType features that should have user control
39 | // Based on http://tiro.com/John/Enabling_Typography_(OTL).pdf
40 | export const userFeatures = {
41 | "smpl": "Simplified Forms",
42 | "trad": "Traditional Forms",
43 | "calt": "Contextual Alternates",
44 | "clig": "Contextual Ligatures",
45 | "jalt": "Justification Alternates",
46 | "liga": "Standard Ligatures",
47 | "rand": "Randomize",
48 | "afrc": "Alternative Fractions",
49 | "c2pc": "Petite Capitals From Capitals",
50 | "c2sc": "Small Capitals From Capitals",
51 | "case": "Case-Sensitive Forms",
52 | "cpsp": "Capital Spacing",
53 | "dlig": "Discretionary Ligatures",
54 | "dnom": "Denominators",
55 | "falt": "Final Glyph on Line Alternates",
56 | "frac": "Fractions",
57 | "halt": "Alternate Half Widths",
58 | "hist": "Historical Forms",
59 | "hwid": "Half Widths",
60 | "lnum": "Lining Figures",
61 | "mgrk": "Mathematical Greek",
62 | "nalt": "Alternate Annotation Forms",
63 | "numr": "Numerators",
64 | "onum": "Oldstyle Figures",
65 | "ordn": "Ordinals",
66 | "ornm": "Ornaments",
67 | "pcap": "Petite Capitals",
68 | "pnum": "Proportional Figures",
69 | "ruby": "Ruby Notation Forms",
70 | "salt": "Stylistic Alternates",
71 | "smcp": "Small Capitals",
72 | "ss01": "Stylistic Set 01",
73 | "ss02": "Stylistic Set 02",
74 | "ss03": "Stylistic Set 03",
75 | "ss04": "Stylistic Set 04",
76 | "ss05": "Stylistic Set 05",
77 | "ss06": "Stylistic Set 06",
78 | "ss07": "Stylistic Set 07",
79 | "ss08": "Stylistic Set 08",
80 | "ss09": "Stylistic Set 09",
81 | "ss10": "Stylistic Set 10",
82 | "ss11": "Stylistic Set 11",
83 | "ss12": "Stylistic Set 12",
84 | "ss13": "Stylistic Set 13",
85 | "ss14": "Stylistic Set 14",
86 | "ss15": "Stylistic Set 15",
87 | "ss16": "Stylistic Set 16",
88 | "ss17": "Stylistic Set 17",
89 | "ss18": "Stylistic Set 18",
90 | "ss19": "Stylistic Set 19",
91 | "ss20": "Stylistic Set 20",
92 | "subs": "Subscript",
93 | "sups": "Superscript",
94 | "swsh": "Swash",
95 | "titl": "Titling",
96 | "tnum": "Tabular Figures",
97 | "unic": "Unicase",
98 | "zero": "Slashed Zero",
99 | "lfbd": "Left Bounds",
100 | "rtbd": "Right Bounds",
101 | "kern": "Kerning"
102 | };
103 |
104 | // Accepts a font file as an ArrayBuffer and return an objects containing parts
105 | // of the 'name', 'OS/2' and 'GSUB' tables.
106 | // The selection of fields is very specific to the need of Stack & Justify
107 | export function parse(buffer) {
108 | const font = {};
109 | const data = new DataView(buffer, 0);
110 | const signature = getTag(data, 0);
111 | let numTables = getUint16(data, 4);
112 | let tableEntries = parseOpenTypeTableEntries(data, numTables);
113 |
114 | for (let tableEntry of tableEntries) {
115 | switch (tableEntry.tag) {
116 | case 'name':
117 | font.name = parseNameTable(data, tableEntry.offset);
118 | break;
119 | case 'OS/2':
120 | font.OS2 = parseOS2Table(data, tableEntry.offset);
121 | break;
122 | case 'GSUB':
123 | font.GSUB = parseGSUBTable(data, tableEntry.offset);
124 | break;
125 | }
126 | }
127 |
128 | return font;
129 | }
130 |
131 | // Format the font tables into a FontInfo object to use in the UI
132 | export function getFontInfo(font, fileName) {
133 | const info = {};
134 |
135 | info.weightClass = font.OS2.usWeightClass;
136 | info.widthClass = font.OS2.usWidthClass;
137 | info.isItalic = font.OS2.fsSelection.italic;
138 |
139 | // Get proper names
140 | info.familyName = font.name.typoFamily || font.name.fontFamily;
141 | info.subfamilyName = font.name.typoSubfamily || font.name.fontSubfamily;
142 | info.fullName = info.familyName + ' ' + info.subfamilyName;
143 | info.fileName = fileName;
144 |
145 | // Create feature list
146 | info.features = [];
147 | if (font.GSUB === undefined) return info;
148 |
149 | for (const feature of font.GSUB.featureList) {
150 | const featureInfo = {
151 | tag: feature.tag,
152 | name: undefined
153 | }
154 | const featureAlreadyExists = info.features.some((_feature) => _feature.tag === feature.tag);
155 |
156 | if (userFeatures[feature.tag] && !featureAlreadyExists) {
157 | // Check if a custom description exists for the feature (usually for ss01-20)
158 | if (feature.uiNameID !== undefined) {
159 | featureInfo.name = font.name[feature.uiNameID];
160 | } else {
161 | featureInfo.name = userFeatures[feature.tag];
162 | }
163 | info.features.push(featureInfo);
164 | }
165 | }
166 |
167 | return info;
168 | }
169 |
170 | function parseOpenTypeTableEntries(data, numTables) {
171 | const tableEntries = [];
172 | let p = 12;
173 | for (let i = 0; i < numTables; i+=1) {
174 | const tag = getTag(data, p);
175 | const offset = getUint32(data, p+8);
176 | const length = getUint32(data, p+12);
177 | tableEntries.push({tag, offset, length, compression: false});
178 | p += 16;
179 | }
180 |
181 | return tableEntries;
182 | }
183 |
184 | function parseNameTable(data, start) {
185 | const table = {};
186 | const p = Parser(data, start);
187 | const format = p.parseUint16();
188 | const recordsNum = p.parseUint16();
189 | const storageOffset = start + p.parseUint16();
190 |
191 |
192 | for (let i = 0; i < recordsNum; i++) {
193 | const platformID = p.parseUint16();
194 | const encodingID = p.parseUint16();
195 | const languageID = p.parseUint16();
196 | const nameID = p.parseUint16();
197 | const byteLength = p.parseUint16();
198 | const recordOffset = storageOffset + p.parseUint16();
199 | const name = nameTableNames[nameID] || nameID;
200 |
201 | if (platformID !== 3 || languageID !== 0x0409) continue;
202 |
203 | let text = getUTF16String(data, recordOffset, byteLength);
204 | table[name] = text;
205 | }
206 |
207 | return table;
208 | }
209 |
210 | function parseGSUBTable(data, start) {
211 | const table = {};
212 | const featureListOffset = start + getUint16(data, start + 6);
213 | const featureCount = getUint16(data, featureListOffset);
214 | const p = Parser(data, featureListOffset + 2);
215 |
216 | table.featureList = [];
217 | for (let i = 0; i < featureCount; i++) {
218 | const featureTag = p.parseTag();
219 | let uiNameID;
220 |
221 | // Some features can have custom descriptions
222 | // The uiNameID points to a string in the name table
223 | const featureOffset = featureListOffset + p.parseUint16();
224 | const featureParamsOffset = getUint16(data, featureOffset);
225 | if (featureParamsOffset !== 0) {
226 | uiNameID = getUint16(data, featureOffset + featureParamsOffset + 2);
227 | }
228 |
229 | table.featureList.push({
230 | tag: featureTag,
231 | uiNameID
232 | });
233 | }
234 |
235 | return table;
236 | }
237 |
238 | function parseOS2Table(data, start) {
239 | const table = {};
240 | table.usWeightClass = getUint16(data, start + 4);
241 | table.usWidthClass = getUint16(data, start + 6);
242 | const fsSelectionRaw = getUint16(data, start + 62);
243 | table.fsSelection = parseFsSelection(fsSelectionRaw);
244 |
245 | return table;
246 | }
247 |
248 | function parseFsSelection(uint16) {
249 | const flags = {}
250 | const bits = uint16.toString(2).padStart(16, '0').split('').reverse().map(bit => bit === "0" ? false : true);
251 | flags.italic = bits[0];
252 | flags.underscore = bits[1];
253 | flags.negative = bits[2];
254 | flags.outlined = bits[3];
255 | flags.strikeout = bits[4];
256 | flags.bold = bits[5];
257 | flags.regular = bits[6];
258 | flags.useTypoMetrics = bits[7];
259 | flags.wws = bits[8];
260 | flags.oblique = bits[9];
261 |
262 | return flags;
263 | }
264 |
265 | function Parser(data, offset) {
266 | let dOffset = 0;
267 |
268 | function parseUint16() {
269 | const val = getUint16(data, offset + dOffset);
270 | dOffset += 2;
271 | return val;
272 | }
273 |
274 | function parseInt16() {
275 | const val = getInt16(data, offset + dOffset);
276 | dOffset += 2;
277 | return val;
278 | }
279 |
280 | function parseTag() {
281 | const val = getTag(data, offset + dOffset);
282 | dOffset += 4;
283 | return val;
284 | }
285 |
286 | return {
287 | data,
288 | offset,
289 | get currentOffset() {
290 | return offset + dOffset;
291 | },
292 | parseUint16,
293 | parseInt16,
294 | parseTag
295 | }
296 | }
297 |
298 | function getUTF16String(data, offset, numBytes) {
299 | const codePoints = [];
300 | const numChars = numBytes/2;
301 | for (let i = 0; i < numChars; i++, offset += 2) {
302 | codePoints[i] = data.getUint16(offset);
303 | }
304 |
305 | return String.fromCharCode.apply(null, codePoints);
306 | }
307 |
308 | function getUint8(data, offset) {
309 | return data.getUint8(offset, false);
310 | }
311 |
312 | function getUint16(data, offset) {
313 | return data.getUint16(offset, false);
314 | }
315 |
316 | function getInt16(data, offset) {
317 | return data.getInt16(offset, false);
318 | }
319 |
320 | function getUint32(data, offset) {
321 | return data.getUint32(offset, false);
322 | }
323 |
324 | function getTag(data, offset) {
325 | let tag = '';
326 | for (let i = offset; i < offset + 4; i += 1) {
327 | tag += String.fromCharCode(data.getInt8(i));
328 | }
329 | return tag;
330 | }
331 |
332 | function getBytes(data, start, numBytes) {
333 | const bytes = [];
334 | for (let i = start; i < start + numBytes; i += 1) {
335 | bytes.push(data.getUint8(i));
336 | }
337 |
338 | return bytes;
339 | }
--------------------------------------------------------------------------------
/js/vendor/mithril.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";function e(e,t,n,r,o,l){return{tag:e,key:t,attrs:n,children:r,text:o,dom:l,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t),void 0,void 0):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t),void 0,void 0)},e.normalizeChildren=function(t){var n=[];if(t.length){for(var r=null!=t[0]&&null!=t[0].key,o=1;o0&&(i.className=l.join(" ")),o[e]={tag:n,attrs:i}}function a(e,t){var r=t.attrs,o=n.call(r,"class"),i=o?r.class:r.className;if(t.tag=e.tag,t.attrs={},!l(e.attrs)&&!l(r)){var a={};for(var u in r)n.call(r,u)&&(a[u]=r[u]);r=a}for(var u in e.attrs)n.call(e.attrs,u)&&"className"!==u&&!n.call(r,u)&&(r[u]=e.attrs[u]);for(var u in null==i&&null==e.attrs.className||(r.className=null!=i?null!=e.attrs.className?String(e.attrs.className)+" "+String(i):i:null!=e.attrs.className?e.attrs.className:null),o&&(r.class=null),r)if(n.call(r,u)&&"key"!==u){t.attrs=r;break}return t}function u(n){if(null==n||"string"!=typeof n&&"function"!=typeof n&&"function"!=typeof n.view)throw Error("The selector must be either a string or a component.");var r=t.apply(1,arguments);return"string"==typeof n&&(r.children=e.normalizeChildren(r.children),"["!==n)?a(o[n]||i(n),r):(r.tag=n,r)}u.trust=function(t){return null==t&&(t=""),e("<",void 0,void 0,t,void 0,void 0)},u.fragment=function(){var n=t.apply(0,arguments);return n.tag="[",n.children=e.normalizeChildren(n.children),n};var s=new WeakMap;var f={delayedRemoval:s,domFor:function*({dom:e,domSize0:t},{generation0:n}={}){if(null!=e)do{const{nextSibling:r}=e;s.get(e)===n&&(yield e,t--),e=r}while(t)}},c=f.delayedRemoval,d=f.domFor,p=function(t){var n,r,o=t&&t.document,l={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function i(e){return e.attrs&&e.attrs.xmlns||l[e.tag]}function a(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function u(e){var t=e.state;try{return this.apply(t,arguments)}finally{a(e,t)}}function s(){try{return o.activeElement}catch(e){return null}}function f(e,t,n,r,o,l,i){for(var a=n;a'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length;for(var a,u=o.createDocumentFragment();a=i.firstChild;)u.appendChild(a);k(e,u,r)}function h(e,t,n,r,o,l){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)f(e,n,0,n.length,r,o,l);else if(null==n||0===n.length)E(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,s=0;if(!i)for(;s=s&&S>=u&&(m=t[k],v=n[S],m.key===v.key);)m!==v&&y(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),k--,S--;for(;k>=s&&S>=u&&(c=t[s],d=n[u],c.key===d.key);)s++,u++,c!==d&&y(e,c,d,r,b(t,s,o),l);for(;k>=s&&S>=u&&u!==S&&c.key===v.key&&m.key===d.key;)x(e,m,h=b(t,s,o)),m!==d&&y(e,m,d,r,h,l),++u<=--S&&x(e,c,o),c!==v&&y(e,c,v,r,o,l),null!=v.dom&&(o=v.dom),s++,m=t[--k],v=n[S],c=t[s],d=n[u];for(;k>=s&&S>=u&&m.key===v.key;)m!==v&&y(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),S--,m=t[--k],v=n[S];if(u>S)E(e,t,s,k+1);else if(s>k)f(e,n,u,S+1,r,o,l);else{var j,A,C=o,O=S-u+1,T=new Array(O),N=0,$=0,L=2147483647,R=0;for($=0;$=u;$--){null==j&&(j=g(t,s,k+1));var I=j[(v=n[$]).key];null!=I&&(L=I>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(w[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=w[r];return w.length=0,t}(T)).length-1,$=S;$>=u;$--)d=n[$],-1===T[$-u]?p(e,d,r,l,o):A[N]===$-u?N--:x(e,d,o),null!=d.dom&&(o=n[$].dom);else for($=S;$>=u;$--)d=n[$],-1===T[$-u]&&p(e,d,r,l,o),null!=d.dom&&(o=n[$].dom)}}else{var P=t.lengthP&&E(e,t,u,t.length),n.length>P&&f(e,n,u,n.length,r,o,l)}}}function y(t,n,r,o,l,a){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=u.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=u.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,e.text=t.text,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&M(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(j(e,t,void 0),v(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(t,n,r,a,l);break;case"[":!function(e,t,n,r,o,l){h(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var $,L=/[A-Z]/g;function R(e){return"-"+e.toLowerCase()}function I(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(L,R)}function P(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(I(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(I(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(I(r))}}function F(){this._=n}function _(e,t,r){if(null!=e.events){if(e.events._=n,e.events[t]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[t]&&e.dom.removeEventListener(t.slice(2),e.events,!1),e.events[t]=void 0):(null==e.events[t]&&e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new F,e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=r)}function D(e,t,n){"function"==typeof e.oninit&&u.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(u.bind(e.oncreate,t))}function M(e,t,n){"function"==typeof e.onupdate&&n.push(u.bind(e.onupdate,t))}return F.prototype=Object.create(null),F.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(t,o,l){if(!t)throw new TypeError("DOM element being rendered to does not exist.");if(null!=$&&t.contains($))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=n,a=$,u=[],f=s(),c=t.namespaceURI;$=t,n="function"==typeof l?l:void 0,r={};try{null==t.vnodes&&(t.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),h(t,t.vnodes,o,u,null,"http://www.w3.org/1999/xhtml"===c?void 0:c),t.vnodes=o,null!=f&&s()!==f&&"function"==typeof f.focus&&f.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(p,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),v=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(p+=e.slice(n,o)),s>=0&&(p+=(n<0?"?":"&")+u.slice(s,c));var m=v(a);return m&&(p+=(n<0&&s<0?"?":"&")+m),r>=0&&(p+=e.slice(r)),f>=0&&(p+=(r<0?"":"&")+u.slice(f)),p},g=function(e,t){function r(e){return new Promise(e)}function o(e,t){for(var r in e.headers)if(n.call(e.headers,r)&&r.toLowerCase()===t)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(t,r){return new Promise((function(l,i){t=y(t,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,m=!1,v=d,h=d.abort;for(var g in d.abort=function(){p=!0,h.call(this)},d.open(u,t,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)n.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var n,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(t),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f {
8 | for (let i = 0; i < workerCount; i++) {
9 | const worker = new Worker('js/wordgenerator/worker.js', {type: 'module'});
10 | worker.postMessage({type: 'load-module', module: hbModule});
11 |
12 | workers[i] = {
13 | worker,
14 | available: true
15 | };
16 | }
17 | });
18 |
19 | function handleNextMessage() {
20 | if (queue.length) {
21 | const message = queue.shift();
22 | postMessage(message.message, message.promise, message.trigger);
23 | }
24 | }
25 |
26 | function queueMessage(message, promise) {
27 | queue.push({message, promise});
28 | }
29 |
30 | function postMessage(message, promise) {
31 | const worker = workers.find(worker => worker.available);
32 |
33 | if (!promise) promise = Deferred();
34 |
35 | if (!worker) {
36 | queueMessage(message, promise);
37 | } else {
38 | worker.worker.postMessage(message);
39 | worker.worker.onmessage = (e) => {
40 | worker.available = true;
41 | promise.resolve(e);
42 | handleNextMessage();
43 | }
44 | worker.available = false;
45 | }
46 |
47 | return promise;
48 | }
49 |
50 | return {
51 | postMessage
52 | }
53 | })();
54 |
55 | function Deferred() {
56 | var res, rej;
57 |
58 | var promise = new Promise((resolve, reject) => {
59 | res = resolve;
60 | rej = reject;
61 | });
62 |
63 | promise.resolve = res;
64 | promise.reject = rej;
65 |
66 | return promise;
67 | }
--------------------------------------------------------------------------------
/js/wordgenerator/worker.js:
--------------------------------------------------------------------------------
1 | import { hbjs } from '../harfbuzzjs/hbjs.js';
2 | import { Filters } from '../Filters.js';
3 |
4 | let hb;
5 |
6 | onmessage = async function(e) {
7 | if (e.data.type === 'load-module') {
8 | const hbModule = e.data.module;
9 | hb = await WebAssembly.instantiate(hbModule, {}).then((instance) => {
10 | return hbjs(instance);
11 | });
12 | }
13 | if (e.data.type === 'sort') {
14 | const words = e.data.words;
15 | const fontBuffer = e.data.fontBuffer;
16 | const fontFeaturesSettings = e.data.fontFeaturesSettings;
17 | const blob = hb.createBlob(fontBuffer)
18 | const face = hb.createFace(blob, 0);
19 | const font = hb.createFont(face);
20 |
21 | const sortedWords = measureWords(words, font, 100, fontFeaturesSettings);
22 | postMessage(sortedWords);
23 | }
24 | };
25 |
26 |
27 | function measureWords(words, font, size, fontFeaturesSettings) {
28 | const scale = 1/(1000/size);
29 | const sortedWords = {};
30 | sortedWords.minWidth = Infinity;
31 | font.setScale(1000, 1000);
32 |
33 | for (let filter of Filters) {
34 | sortedWords[filter.name] = {};
35 |
36 | for (let word of words) {
37 | let filteredWord = filter.apply(word);
38 | let width = Math.floor(measureText(filteredWord, font, scale, fontFeaturesSettings));
39 | if (sortedWords[filter.name][width] === undefined) {
40 | sortedWords[filter.name][width] = []
41 | }
42 | sortedWords[filter.name][width].push(filteredWord)
43 |
44 | if (width < sortedWords.minWidth) sortedWords.minWidth = width;
45 | }
46 |
47 | sortedWords.spaceWidth = Math.floor(measureText(' ', font, scale, fontFeaturesSettings));
48 | }
49 |
50 | return sortedWords;
51 | }
52 |
53 | function measureText(str, font, scale, fontFeaturesSettings) {
54 | const buffer = hb.createBuffer();
55 | buffer.addText(str);
56 | buffer.guessSegmentProperties();
57 | hb.shape(font, buffer, fontFeaturesSettings);
58 | const result = buffer.json();
59 | buffer.destroy();
60 | return result.reduce((acc, curr) => acc + curr.ax, 0)*scale;
61 | }
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | GNU General Public License
2 | ==========================
3 |
4 | _Version 3, 29 June 2007_
5 | _Copyright © 2007 Free Software Foundation, Inc. <>_
6 |
7 | Everyone is permitted to copy and distribute verbatim copies of this license
8 | document, but changing it is not allowed.
9 |
10 | ## Preamble
11 |
12 | The GNU General Public License is a free, copyleft license for software and other
13 | kinds of works.
14 |
15 | The licenses for most software and other practical works are designed to take away
16 | your freedom to share and change the works. By contrast, the GNU General Public
17 | License is intended to guarantee your freedom to share and change all versions of a
18 | program--to make sure it remains free software for all its users. We, the Free
19 | Software Foundation, use the GNU General Public License for most of our software; it
20 | applies also to any other work released this way by its authors. You can apply it to
21 | your programs, too.
22 |
23 | When we speak of free software, we are referring to freedom, not price. Our General
24 | Public Licenses are designed to make sure that you have the freedom to distribute
25 | copies of free software (and charge for them if you wish), that you receive source
26 | code or can get it if you want it, that you can change the software or use pieces of
27 | it in new free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you these rights or
30 | asking you to surrender the rights. Therefore, you have certain responsibilities if
31 | you distribute copies of the software, or if you modify it: responsibilities to
32 | respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether gratis or for a fee,
35 | you must pass on to the recipients the same freedoms that you received. You must make
36 | sure that they, too, receive or can get the source code. And you must show them these
37 | terms so they know their rights.
38 |
39 | Developers that use the GNU GPL protect your rights with two steps: **(1)** assert
40 | copyright on the software, and **(2)** offer you this License giving you legal permission
41 | to copy, distribute and/or modify it.
42 |
43 | For the developers' and authors' protection, the GPL clearly explains that there is
44 | no warranty for this free software. For both users' and authors' sake, the GPL
45 | requires that modified versions be marked as changed, so that their problems will not
46 | be attributed erroneously to authors of previous versions.
47 |
48 | Some devices are designed to deny users access to install or run modified versions of
49 | the software inside them, although the manufacturer can do so. This is fundamentally
50 | incompatible with the aim of protecting users' freedom to change the software. The
51 | systematic pattern of such abuse occurs in the area of products for individuals to
52 | use, which is precisely where it is most unacceptable. Therefore, we have designed
53 | this version of the GPL to prohibit the practice for those products. If such problems
54 | arise substantially in other domains, we stand ready to extend this provision to
55 | those domains in future versions of the GPL, as needed to protect the freedom of
56 | users.
57 |
58 | Finally, every program is threatened constantly by software patents. States should
59 | not allow patents to restrict development and use of software on general-purpose
60 | computers, but in those that do, we wish to avoid the special danger that patents
61 | applied to a free program could make it effectively proprietary. To prevent this, the
62 | GPL assures that patents cannot be used to render the program non-free.
63 |
64 | The precise terms and conditions for copying, distribution and modification follow.
65 |
66 | ## TERMS AND CONDITIONS
67 |
68 | ### 0. Definitions
69 |
70 | “This License” refers to version 3 of the GNU General Public License.
71 |
72 | “Copyright” also means copyright-like laws that apply to other kinds of
73 | works, such as semiconductor masks.
74 |
75 | “The Program” refers to any copyrightable work licensed under this
76 | License. Each licensee is addressed as “you”. “Licensees” and
77 | “recipients” may be individuals or organizations.
78 |
79 | To “modify” a work means to copy from or adapt all or part of the work in
80 | a fashion requiring copyright permission, other than the making of an exact copy. The
81 | resulting work is called a “modified version” of the earlier work or a
82 | work “based on” the earlier work.
83 |
84 | A “covered work” means either the unmodified Program or a work based on
85 | the Program.
86 |
87 | To “propagate” a work means to do anything with it that, without
88 | permission, would make you directly or secondarily liable for infringement under
89 | applicable copyright law, except executing it on a computer or modifying a private
90 | copy. Propagation includes copying, distribution (with or without modification),
91 | making available to the public, and in some countries other activities as well.
92 |
93 | To “convey” a work means any kind of propagation that enables other
94 | parties to make or receive copies. Mere interaction with a user through a computer
95 | network, with no transfer of a copy, is not conveying.
96 |
97 | An interactive user interface displays “Appropriate Legal Notices” to the
98 | extent that it includes a convenient and prominently visible feature that **(1)**
99 | displays an appropriate copyright notice, and **(2)** tells the user that there is no
100 | warranty for the work (except to the extent that warranties are provided), that
101 | licensees may convey the work under this License, and how to view a copy of this
102 | License. If the interface presents a list of user commands or options, such as a
103 | menu, a prominent item in the list meets this criterion.
104 |
105 | ### 1. Source Code
106 |
107 | The “source code” for a work means the preferred form of the work for
108 | making modifications to it. “Object code” means any non-source form of a
109 | work.
110 |
111 | A “Standard Interface” means an interface that either is an official
112 | standard defined by a recognized standards body, or, in the case of interfaces
113 | specified for a particular programming language, one that is widely used among
114 | developers working in that language.
115 |
116 | The “System Libraries” of an executable work include anything, other than
117 | the work as a whole, that **(a)** is included in the normal form of packaging a Major
118 | Component, but which is not part of that Major Component, and **(b)** serves only to
119 | enable use of the work with that Major Component, or to implement a Standard
120 | Interface for which an implementation is available to the public in source code form.
121 | A “Major Component”, in this context, means a major essential component
122 | (kernel, window system, and so on) of the specific operating system (if any) on which
123 | the executable work runs, or a compiler used to produce the work, or an object code
124 | interpreter used to run it.
125 |
126 | The “Corresponding Source” for a work in object code form means all the
127 | source code needed to generate, install, and (for an executable work) run the object
128 | code and to modify the work, including scripts to control those activities. However,
129 | it does not include the work's System Libraries, or general-purpose tools or
130 | generally available free programs which are used unmodified in performing those
131 | activities but which are not part of the work. For example, Corresponding Source
132 | includes interface definition files associated with source files for the work, and
133 | the source code for shared libraries and dynamically linked subprograms that the work
134 | is specifically designed to require, such as by intimate data communication or
135 | control flow between those subprograms and other parts of the work.
136 |
137 | The Corresponding Source need not include anything that users can regenerate
138 | automatically from other parts of the Corresponding Source.
139 |
140 | The Corresponding Source for a work in source code form is that same work.
141 |
142 | ### 2. Basic Permissions
143 |
144 | All rights granted under this License are granted for the term of copyright on the
145 | Program, and are irrevocable provided the stated conditions are met. This License
146 | explicitly affirms your unlimited permission to run the unmodified Program. The
147 | output from running a covered work is covered by this License only if the output,
148 | given its content, constitutes a covered work. This License acknowledges your rights
149 | of fair use or other equivalent, as provided by copyright law.
150 |
151 | You may make, run and propagate covered works that you do not convey, without
152 | conditions so long as your license otherwise remains in force. You may convey covered
153 | works to others for the sole purpose of having them make modifications exclusively
154 | for you, or provide you with facilities for running those works, provided that you
155 | comply with the terms of this License in conveying all material for which you do not
156 | control copyright. Those thus making or running the covered works for you must do so
157 | exclusively on your behalf, under your direction and control, on terms that prohibit
158 | them from making any copies of your copyrighted material outside their relationship
159 | with you.
160 |
161 | Conveying under any other circumstances is permitted solely under the conditions
162 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
163 |
164 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law
165 |
166 | No covered work shall be deemed part of an effective technological measure under any
167 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty
168 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention
169 | of such measures.
170 |
171 | When you convey a covered work, you waive any legal power to forbid circumvention of
172 | technological measures to the extent such circumvention is effected by exercising
173 | rights under this License with respect to the covered work, and you disclaim any
174 | intention to limit operation or modification of the work as a means of enforcing,
175 | against the work's users, your or third parties' legal rights to forbid circumvention
176 | of technological measures.
177 |
178 | ### 4. Conveying Verbatim Copies
179 |
180 | You may convey verbatim copies of the Program's source code as you receive it, in any
181 | medium, provided that you conspicuously and appropriately publish on each copy an
182 | appropriate copyright notice; keep intact all notices stating that this License and
183 | any non-permissive terms added in accord with section 7 apply to the code; keep
184 | intact all notices of the absence of any warranty; and give all recipients a copy of
185 | this License along with the Program.
186 |
187 | You may charge any price or no price for each copy that you convey, and you may offer
188 | support or warranty protection for a fee.
189 |
190 | ### 5. Conveying Modified Source Versions
191 |
192 | You may convey a work based on the Program, or the modifications to produce it from
193 | the Program, in the form of source code under the terms of section 4, provided that
194 | you also meet all of these conditions:
195 |
196 | * **a)** The work must carry prominent notices stating that you modified it, and giving a
197 | relevant date.
198 | * **b)** The work must carry prominent notices stating that it is released under this
199 | License and any conditions added under section 7. This requirement modifies the
200 | requirement in section 4 to “keep intact all notices”.
201 | * **c)** You must license the entire work, as a whole, under this License to anyone who
202 | comes into possession of a copy. This License will therefore apply, along with any
203 | applicable section 7 additional terms, to the whole of the work, and all its parts,
204 | regardless of how they are packaged. This License gives no permission to license the
205 | work in any other way, but it does not invalidate such permission if you have
206 | separately received it.
207 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal
208 | Notices; however, if the Program has interactive interfaces that do not display
209 | Appropriate Legal Notices, your work need not make them do so.
210 |
211 | A compilation of a covered work with other separate and independent works, which are
212 | not by their nature extensions of the covered work, and which are not combined with
213 | it such as to form a larger program, in or on a volume of a storage or distribution
214 | medium, is called an “aggregate” if the compilation and its resulting
215 | copyright are not used to limit the access or legal rights of the compilation's users
216 | beyond what the individual works permit. Inclusion of a covered work in an aggregate
217 | does not cause this License to apply to the other parts of the aggregate.
218 |
219 | ### 6. Conveying Non-Source Forms
220 |
221 | You may convey a covered work in object code form under the terms of sections 4 and
222 | 5, provided that you also convey the machine-readable Corresponding Source under the
223 | terms of this License, in one of these ways:
224 |
225 | * **a)** Convey the object code in, or embodied in, a physical product (including a
226 | physical distribution medium), accompanied by the Corresponding Source fixed on a
227 | durable physical medium customarily used for software interchange.
228 | * **b)** Convey the object code in, or embodied in, a physical product (including a
229 | physical distribution medium), accompanied by a written offer, valid for at least
230 | three years and valid for as long as you offer spare parts or customer support for
231 | that product model, to give anyone who possesses the object code either **(1)** a copy of
232 | the Corresponding Source for all the software in the product that is covered by this
233 | License, on a durable physical medium customarily used for software interchange, for
234 | a price no more than your reasonable cost of physically performing this conveying of
235 | source, or **(2)** access to copy the Corresponding Source from a network server at no
236 | charge.
237 | * **c)** Convey individual copies of the object code with a copy of the written offer to
238 | provide the Corresponding Source. This alternative is allowed only occasionally and
239 | noncommercially, and only if you received the object code with such an offer, in
240 | accord with subsection 6b.
241 | * **d)** Convey the object code by offering access from a designated place (gratis or for
242 | a charge), and offer equivalent access to the Corresponding Source in the same way
243 | through the same place at no further charge. You need not require recipients to copy
244 | the Corresponding Source along with the object code. If the place to copy the object
245 | code is a network server, the Corresponding Source may be on a different server
246 | (operated by you or a third party) that supports equivalent copying facilities,
247 | provided you maintain clear directions next to the object code saying where to find
248 | the Corresponding Source. Regardless of what server hosts the Corresponding Source,
249 | you remain obligated to ensure that it is available for as long as needed to satisfy
250 | these requirements.
251 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform
252 | other peers where the object code and Corresponding Source of the work are being
253 | offered to the general public at no charge under subsection 6d.
254 |
255 | A separable portion of the object code, whose source code is excluded from the
256 | Corresponding Source as a System Library, need not be included in conveying the
257 | object code work.
258 |
259 | A “User Product” is either **(1)** a “consumer product”, which
260 | means any tangible personal property which is normally used for personal, family, or
261 | household purposes, or **(2)** anything designed or sold for incorporation into a
262 | dwelling. In determining whether a product is a consumer product, doubtful cases
263 | shall be resolved in favor of coverage. For a particular product received by a
264 | particular user, “normally used” refers to a typical or common use of
265 | that class of product, regardless of the status of the particular user or of the way
266 | in which the particular user actually uses, or expects or is expected to use, the
267 | product. A product is a consumer product regardless of whether the product has
268 | substantial commercial, industrial or non-consumer uses, unless such uses represent
269 | the only significant mode of use of the product.
270 |
271 | “Installation Information” for a User Product means any methods,
272 | procedures, authorization keys, or other information required to install and execute
273 | modified versions of a covered work in that User Product from a modified version of
274 | its Corresponding Source. The information must suffice to ensure that the continued
275 | functioning of the modified object code is in no case prevented or interfered with
276 | solely because modification has been made.
277 |
278 | If you convey an object code work under this section in, or with, or specifically for
279 | use in, a User Product, and the conveying occurs as part of a transaction in which
280 | the right of possession and use of the User Product is transferred to the recipient
281 | in perpetuity or for a fixed term (regardless of how the transaction is
282 | characterized), the Corresponding Source conveyed under this section must be
283 | accompanied by the Installation Information. But this requirement does not apply if
284 | neither you nor any third party retains the ability to install modified object code
285 | on the User Product (for example, the work has been installed in ROM).
286 |
287 | The requirement to provide Installation Information does not include a requirement to
288 | continue to provide support service, warranty, or updates for a work that has been
289 | modified or installed by the recipient, or for the User Product in which it has been
290 | modified or installed. Access to a network may be denied when the modification itself
291 | materially and adversely affects the operation of the network or violates the rules
292 | and protocols for communication across the network.
293 |
294 | Corresponding Source conveyed, and Installation Information provided, in accord with
295 | this section must be in a format that is publicly documented (and with an
296 | implementation available to the public in source code form), and must require no
297 | special password or key for unpacking, reading or copying.
298 |
299 | ### 7. Additional Terms
300 |
301 | “Additional permissions” are terms that supplement the terms of this
302 | License by making exceptions from one or more of its conditions. Additional
303 | permissions that are applicable to the entire Program shall be treated as though they
304 | were included in this License, to the extent that they are valid under applicable
305 | law. If additional permissions apply only to part of the Program, that part may be
306 | used separately under those permissions, but the entire Program remains governed by
307 | this License without regard to the additional permissions.
308 |
309 | When you convey a copy of a covered work, you may at your option remove any
310 | additional permissions from that copy, or from any part of it. (Additional
311 | permissions may be written to require their own removal in certain cases when you
312 | modify the work.) You may place additional permissions on material, added by you to a
313 | covered work, for which you have or can give appropriate copyright permission.
314 |
315 | Notwithstanding any other provision of this License, for material you add to a
316 | covered work, you may (if authorized by the copyright holders of that material)
317 | supplement the terms of this License with terms:
318 |
319 | * **a)** Disclaiming warranty or limiting liability differently from the terms of
320 | sections 15 and 16 of this License; or
321 | * **b)** Requiring preservation of specified reasonable legal notices or author
322 | attributions in that material or in the Appropriate Legal Notices displayed by works
323 | containing it; or
324 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that
325 | modified versions of such material be marked in reasonable ways as different from the
326 | original version; or
327 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the
328 | material; or
329 | * **e)** Declining to grant rights under trademark law for use of some trade names,
330 | trademarks, or service marks; or
331 | * **f)** Requiring indemnification of licensors and authors of that material by anyone
332 | who conveys the material (or modified versions of it) with contractual assumptions of
333 | liability to the recipient, for any liability that these contractual assumptions
334 | directly impose on those licensors and authors.
335 |
336 | All other non-permissive additional terms are considered “further
337 | restrictions” within the meaning of section 10. If the Program as you received
338 | it, or any part of it, contains a notice stating that it is governed by this License
339 | along with a term that is a further restriction, you may remove that term. If a
340 | license document contains a further restriction but permits relicensing or conveying
341 | under this License, you may add to a covered work material governed by the terms of
342 | that license document, provided that the further restriction does not survive such
343 | relicensing or conveying.
344 |
345 | If you add terms to a covered work in accord with this section, you must place, in
346 | the relevant source files, a statement of the additional terms that apply to those
347 | files, or a notice indicating where to find the applicable terms.
348 |
349 | Additional terms, permissive or non-permissive, may be stated in the form of a
350 | separately written license, or stated as exceptions; the above requirements apply
351 | either way.
352 |
353 | ### 8. Termination
354 |
355 | You may not propagate or modify a covered work except as expressly provided under
356 | this License. Any attempt otherwise to propagate or modify it is void, and will
357 | automatically terminate your rights under this License (including any patent licenses
358 | granted under the third paragraph of section 11).
359 |
360 | However, if you cease all violation of this License, then your license from a
361 | particular copyright holder is reinstated **(a)** provisionally, unless and until the
362 | copyright holder explicitly and finally terminates your license, and **(b)** permanently,
363 | if the copyright holder fails to notify you of the violation by some reasonable means
364 | prior to 60 days after the cessation.
365 |
366 | Moreover, your license from a particular copyright holder is reinstated permanently
367 | if the copyright holder notifies you of the violation by some reasonable means, this
368 | is the first time you have received notice of violation of this License (for any
369 | work) from that copyright holder, and you cure the violation prior to 30 days after
370 | your receipt of the notice.
371 |
372 | Termination of your rights under this section does not terminate the licenses of
373 | parties who have received copies or rights from you under this License. If your
374 | rights have been terminated and not permanently reinstated, you do not qualify to
375 | receive new licenses for the same material under section 10.
376 |
377 | ### 9. Acceptance Not Required for Having Copies
378 |
379 | You are not required to accept this License in order to receive or run a copy of the
380 | Program. Ancillary propagation of a covered work occurring solely as a consequence of
381 | using peer-to-peer transmission to receive a copy likewise does not require
382 | acceptance. However, nothing other than this License grants you permission to
383 | propagate or modify any covered work. These actions infringe copyright if you do not
384 | accept this License. Therefore, by modifying or propagating a covered work, you
385 | indicate your acceptance of this License to do so.
386 |
387 | ### 10. Automatic Licensing of Downstream Recipients
388 |
389 | Each time you convey a covered work, the recipient automatically receives a license
390 | from the original licensors, to run, modify and propagate that work, subject to this
391 | License. You are not responsible for enforcing compliance by third parties with this
392 | License.
393 |
394 | An “entity transaction” is a transaction transferring control of an
395 | organization, or substantially all assets of one, or subdividing an organization, or
396 | merging organizations. If propagation of a covered work results from an entity
397 | transaction, each party to that transaction who receives a copy of the work also
398 | receives whatever licenses to the work the party's predecessor in interest had or
399 | could give under the previous paragraph, plus a right to possession of the
400 | Corresponding Source of the work from the predecessor in interest, if the predecessor
401 | has it or can get it with reasonable efforts.
402 |
403 | You may not impose any further restrictions on the exercise of the rights granted or
404 | affirmed under this License. For example, you may not impose a license fee, royalty,
405 | or other charge for exercise of rights granted under this License, and you may not
406 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging
407 | that any patent claim is infringed by making, using, selling, offering for sale, or
408 | importing the Program or any portion of it.
409 |
410 | ### 11. Patents
411 |
412 | A “contributor” is a copyright holder who authorizes use under this
413 | License of the Program or a work on which the Program is based. The work thus
414 | licensed is called the contributor's “contributor version”.
415 |
416 | A contributor's “essential patent claims” are all patent claims owned or
417 | controlled by the contributor, whether already acquired or hereafter acquired, that
418 | would be infringed by some manner, permitted by this License, of making, using, or
419 | selling its contributor version, but do not include claims that would be infringed
420 | only as a consequence of further modification of the contributor version. For
421 | purposes of this definition, “control” includes the right to grant patent
422 | sublicenses in a manner consistent with the requirements of this License.
423 |
424 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license
425 | under the contributor's essential patent claims, to make, use, sell, offer for sale,
426 | import and otherwise run, modify and propagate the contents of its contributor
427 | version.
428 |
429 | In the following three paragraphs, a “patent license” is any express
430 | agreement or commitment, however denominated, not to enforce a patent (such as an
431 | express permission to practice a patent or covenant not to sue for patent
432 | infringement). To “grant” such a patent license to a party means to make
433 | such an agreement or commitment not to enforce a patent against the party.
434 |
435 | If you convey a covered work, knowingly relying on a patent license, and the
436 | Corresponding Source of the work is not available for anyone to copy, free of charge
437 | and under the terms of this License, through a publicly available network server or
438 | other readily accessible means, then you must either **(1)** cause the Corresponding
439 | Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the
440 | patent license for this particular work, or **(3)** arrange, in a manner consistent with
441 | the requirements of this License, to extend the patent license to downstream
442 | recipients. “Knowingly relying” means you have actual knowledge that, but
443 | for the patent license, your conveying the covered work in a country, or your
444 | recipient's use of the covered work in a country, would infringe one or more
445 | identifiable patents in that country that you have reason to believe are valid.
446 |
447 | If, pursuant to or in connection with a single transaction or arrangement, you
448 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent
449 | license to some of the parties receiving the covered work authorizing them to use,
450 | propagate, modify or convey a specific copy of the covered work, then the patent
451 | license you grant is automatically extended to all recipients of the covered work and
452 | works based on it.
453 |
454 | A patent license is “discriminatory” if it does not include within the
455 | scope of its coverage, prohibits the exercise of, or is conditioned on the
456 | non-exercise of one or more of the rights that are specifically granted under this
457 | License. You may not convey a covered work if you are a party to an arrangement with
458 | a third party that is in the business of distributing software, under which you make
459 | payment to the third party based on the extent of your activity of conveying the
460 | work, and under which the third party grants, to any of the parties who would receive
461 | the covered work from you, a discriminatory patent license **(a)** in connection with
462 | copies of the covered work conveyed by you (or copies made from those copies), or **(b)**
463 | primarily for and in connection with specific products or compilations that contain
464 | the covered work, unless you entered into that arrangement, or that patent license
465 | was granted, prior to 28 March 2007.
466 |
467 | Nothing in this License shall be construed as excluding or limiting any implied
468 | license or other defenses to infringement that may otherwise be available to you
469 | under applicable patent law.
470 |
471 | ### 12. No Surrender of Others' Freedom
472 |
473 | If conditions are imposed on you (whether by court order, agreement or otherwise)
474 | that contradict the conditions of this License, they do not excuse you from the
475 | conditions of this License. If you cannot convey a covered work so as to satisfy
476 | simultaneously your obligations under this License and any other pertinent
477 | obligations, then as a consequence you may not convey it at all. For example, if you
478 | agree to terms that obligate you to collect a royalty for further conveying from
479 | those to whom you convey the Program, the only way you could satisfy both those terms
480 | and this License would be to refrain entirely from conveying the Program.
481 |
482 | ### 13. Use with the GNU Affero General Public License
483 |
484 | Notwithstanding any other provision of this License, you have permission to link or
485 | combine any covered work with a work licensed under version 3 of the GNU Affero
486 | General Public License into a single combined work, and to convey the resulting work.
487 | The terms of this License will continue to apply to the part which is the covered
488 | work, but the special requirements of the GNU Affero General Public License, section
489 | 13, concerning interaction through a network will apply to the combination as such.
490 |
491 | ### 14. Revised Versions of this License
492 |
493 | The Free Software Foundation may publish revised and/or new versions of the GNU
494 | General Public License from time to time. Such new versions will be similar in spirit
495 | to the present version, but may differ in detail to address new problems or concerns.
496 |
497 | Each version is given a distinguishing version number. If the Program specifies that
498 | a certain numbered version of the GNU General Public License “or any later
499 | version” applies to it, you have the option of following the terms and
500 | conditions either of that numbered version or of any later version published by the
501 | Free Software Foundation. If the Program does not specify a version number of the GNU
502 | General Public License, you may choose any version ever published by the Free
503 | Software Foundation.
504 |
505 | If the Program specifies that a proxy can decide which future versions of the GNU
506 | General Public License can be used, that proxy's public statement of acceptance of a
507 | version permanently authorizes you to choose that version for the Program.
508 |
509 | Later license versions may give you additional or different permissions. However, no
510 | additional obligations are imposed on any author or copyright holder as a result of
511 | your choosing to follow a later version.
512 |
513 | ### 15. Disclaimer of Warranty
514 |
515 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
516 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
517 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER
518 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
519 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
520 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
521 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
522 |
523 | ### 16. Limitation of Liability
524 |
525 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
526 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
527 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
528 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
529 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE
530 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE
531 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
532 | POSSIBILITY OF SUCH DAMAGES.
533 |
534 | ### 17. Interpretation of Sections 15 and 16
535 |
536 | If the disclaimer of warranty and limitation of liability provided above cannot be
537 | given local legal effect according to their terms, reviewing courts shall apply local
538 | law that most closely approximates an absolute waiver of all civil liability in
539 | connection with the Program, unless a warranty or assumption of liability accompanies
540 | a copy of the Program in return for a fee.
541 |
542 | _END OF TERMS AND CONDITIONS_
543 |
544 | ## How to Apply These Terms to Your New Programs
545 |
546 | If you develop a new program, and you want it to be of the greatest possible use to
547 | the public, the best way to achieve this is to make it free software which everyone
548 | can redistribute and change under these terms.
549 |
550 | To do so, attach the following notices to the program. It is safest to attach them
551 | to the start of each source file to most effectively state the exclusion of warranty;
552 | and each file should have at least the “copyright” line and a pointer to
553 | where the full notice is found.
554 |
555 |
556 | Copyright (C)
557 |
558 | This program is free software: you can redistribute it and/or modify
559 | it under the terms of the GNU General Public License as published by
560 | the Free Software Foundation, either version 3 of the License, or
561 | (at your option) any later version.
562 |
563 | This program is distributed in the hope that it will be useful,
564 | but WITHOUT ANY WARRANTY; without even the implied warranty of
565 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
566 | GNU General Public License for more details.
567 |
568 | You should have received a copy of the GNU General Public License
569 | along with this program. If not, see .
570 |
571 | Also add information on how to contact you by electronic and paper mail.
572 |
573 | If the program does terminal interaction, make it output a short notice like this
574 | when it starts in an interactive mode:
575 |
576 | Copyright (C)
577 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
578 | This is free software, and you are welcome to redistribute it
579 | under certain conditions; type 'show c' for details.
580 |
581 | The hypothetical commands `show w` and `show c` should show the appropriate parts of
582 | the General Public License. Of course, your program's commands might be different;
583 | for a GUI interface, you would use an “about box”.
584 |
585 | You should also get your employer (if you work as a programmer) or school, if any, to
586 | sign a “copyright disclaimer” for the program, if necessary. For more
587 | information on this, and how to apply and follow the GNU GPL, see
588 | <>.
589 |
590 | The GNU General Public License does not permit incorporating your program into
591 | proprietary programs. If your program is a subroutine library, you may consider it
592 | more useful to permit linking proprietary applications with the library. If this is
593 | what you want to do, use the GNU Lesser General Public License instead of this
594 | License. But first, please read
595 | <>.
596 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Stack & Justify
2 | ===============
3 |
4 | 
5 |
6 | Stack & Justify is a tool to help create type specimens by finding words or phrases of the same width. It is free to use and distributed under GPLv3 license.
7 |
8 | Font files are not uploaded, they remain stored locally in your browser.
9 |
10 | For a similar tool, also check Mass Driver’s Waterfall from which this tool was inspired.
--------------------------------------------------------------------------------
/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/words/wikipedia/is_wikipedia.json:
--------------------------------------------------------------------------------
1 | {
2 | "words": [
3 | "Forsíða",
4 | "Carles Puigdemont",
5 | "Marie Antoinette",
6 | "Matt Groening",
7 | "Fiann Paul",
8 | "Fullveldisdagurinn",
9 | "Upernavik",
10 | "Fríverslunarsamtök Evrópu",
11 | "Alfreð Finnbogason",
12 | "Bandaríska frelsisstríðið",
13 | "Mynstur",
14 | "Fluga",
15 | "Forgotten Lores",
16 | "Gjálp og Greip",
17 | "Guðbjörg Matthíasdóttir",
18 | "Hagfræði",
19 | "Haust",
20 | "Háskóli Íslands",
21 | "Mannshvörf á Íslandi",
22 | "Mervíkingar",
23 | "Milton Friedman",
24 | "Palestína",
25 | "Taylor Swift",
26 | "Wiki",
27 | "Wikiheimild",
28 | "YouTube",
29 | "Írafár",
30 | "Íslensku tónlistarverðlaunin 2023",
31 | "-",
32 | "1214",
33 | "1243",
34 | "1752",
35 | "1994",
36 | "20. nóvember",
37 | "Almenningur",
38 | "Alþjóðasiglingamálastofnunin",
39 | "Apple Macintosh",
40 | "Baldur",
41 | "Bangladess",
42 | "Bor",
43 | "Bragi",
44 | "Búri",
45 | "DV",
46 | "Dellingur",
47 | "Dóminíka",
48 | "Eiginfjárhlutfall",
49 | "Eigið fé",
50 | "Einhildur",
51 | "Eldgosið við Fagradalsfjall 2021",
52 | "Forseti",
53 | "Geimgreftrun",
54 | "Gervigreind",
55 | "Gildishlaðinn texti",
56 | "Gjálpargosið",
57 | "Greni",
58 | "Gullbringusýsla",
59 | "Gunnar Dal",
60 | "Heimdallur",
61 | "Hernám Íslands",
62 | "Hákon Sverrisson",
63 | "IP-tala",
64 | "Iðnbyltingin",
65 | "Jarðvarmavirkjun",
66 | "Joe Biden",
67 | "Joseph Conrad",
68 | "Jón Sigurðsson frá Kaldaðarnesi",
69 | "Jövu-nashyrningur",
70 | "Karlungar",
71 | "Krímstríðið",
72 | "Leirkeragerð",
73 | "Lind",
74 | "Lögbundnir frídagar á Íslandi",
75 | "Lögmaður",
76 | "Mbl.is",
77 | "Metri",
78 | "Miltisbrandur",
79 | "Morgunblaðið",
80 | "Mosar",
81 | "Mínóísk menning",
82 | "Nýlenda",
83 | "Palestínuríki",
84 | "Rangárvallasýsla",
85 | "Richard Stallman",
86 | "Salka Valka",
87 | "Scafell Pike",
88 | "Scaled and Icy",
89 | "Shane MacGowan",
90 | "Sjálfstæðisyfirlýsing Bandaríkjanna",
91 | "Sturlungaöld",
92 | "Stétt",
93 | "Sýslur Íslands",
94 | "Teikniborð",
95 | "The Pirate Bay",
96 | "Timbur",
97 | "Tjörnes",
98 | "Upplýsingar",
99 | "Vísir",
100 | "Wayback Machine",
101 | "Ísland",
102 | "Ísland í seinni heimsstyrjöldinni",
103 | "Íslensk sveitarfélög eftir mannfjölda",
104 | "Órestes",
105 | "Þingeyjarsýsla",
106 | "Þórðarhyrna",
107 | ".hm",
108 | ".mk",
109 | "1. deild karla í knattspyrnu 1991",
110 | "1. desember",
111 | "1000",
112 | "1164",
113 | "1211",
114 | "1670",
115 | "1835",
116 | "1856",
117 | "1903",
118 | "1924",
119 | "1925",
120 | "1930",
121 | "1941",
122 | "1945",
123 | "1990",
124 | "4. deild karla í knattspyrnu",
125 | "500",
126 | "8. ágúst",
127 | "ABBA",
128 | "Abraham",
129 | "Adam Sandler",
130 | "Addisonveiki",
131 | "Airbus",
132 | "Akkadíska",
133 | "Aldursgreining með geislunarmælingu",
134 | "Alfræðirit",
135 | "Alkibíades",
136 | "Almannavalsfræði",
137 | "Alsdorf",
138 | "Alþingiskosningar 2021",
139 | "Alþjóðasamningur um öryggi mannslífa á hafinu",
140 | "Andesít",
141 | "Anjem Choudary",
142 | "Armeria duriaei",
143 | "Arnór Sigurðsson",
144 | "Assamíska",
145 | "Athygli",
146 | "Axel",
147 | "Aða",
148 | "Aðall",
149 | "Aðferðaminni",
150 | "Bangsímon",
151 | "Bankahrunið á Íslandi",
152 | "Barein",
153 | "Basalt",
154 | "Beiting",
155 | "Benedikt Erlingsson",
156 | "Berklar",
157 | "Bermúdaþríhyrningurinn",
158 | "Berta",
159 | "Betlehemstjarna",
160 | "Bikarkeppni kvenna í knattspyrnu",
161 | "Binni Glee",
162 | "Bishkek",
163 | "Bjarni Pálsson",
164 | "Björgunarbátur",
165 | "Blaðamennska",
166 | "Bluetooth",
167 | "Bláa lónið",
168 | "Blágreni",
169 | "Blóðberg",
170 | "Blóðrásarkerfið",
171 | "Blóðörn",
172 | "Bolludagur",
173 | "Borgarfjarðarsýsla",
174 | "Botsvana",
175 | "Boys Like Girls",
176 | "Breiðablik UBK",
177 | "Brekkugoði",
178 | "Brendon Small",
179 | "Bretland",
180 | "Breyta",
181 | "Brissafi",
182 | "Bruce Dickinson",
183 | "Brynja Þorgeirsdóttir",
184 | "Bríet Bjarnhéðinsdóttir",
185 | "Brúðkaupsafmæli",
186 | "Bær",
187 | "Bók",
188 | "Búddismi",
189 | "Búrúndí",
190 | "Bútan",
191 | "Charles Cecil",
192 | "Charlie Hebdo",
193 | "David Villa",
194 | "Deildartunguhver",
195 | "Demókrítos",
196 | "Desembristauppreisnin",
197 | "Disney+",
198 | "Douglas Adams",
199 | "Droplaug",
200 | "Dulfrævingar",
201 | "Dyflinni",
202 | "Dósaverksmiðjan",
203 | "Dúbaí",
204 | "E. E. Evans-Pritchard",
205 | "Edda Heiðrún Backman",
206 | "Edduverðlaunin",
207 | "Efndir in natura",
208 | "Egg",
209 | "Egon Friedell",
210 | "Egypska byltingin 2011",
211 | "Eiginnafn",
212 | "Eign",
213 | "Eimreiðarhópurinn",
214 | "Einar Kárason",
215 | "Einhverfa",
216 | "Einkaskóli",
217 | "Eiturvirkni",
218 | "Eiturþörungar",
219 | "Eldgosið við Litla-Hrút 2023",
220 | "Electric Six",
221 | "Elizabeth",
222 | "Emily Dickinson",
223 | "Endurskoðunarstefna",
224 | "Eðlismassi",
225 | "Fastanefndir Alþingis",
226 | "Ferðamaður",
227 | "Fjallagrös",
228 | "Fjallkonan",
229 | "Fjölbrautaskólinn í Breiðholti",
230 | "Fjölgreindakenningin",
231 | "Fjöruspói",
232 | "Flosi Þórðarson",
233 | "Flæðafura",
234 | "Fornbókabúðin",
235 | "Forsætisráðherra Íslands",
236 | "Frans 2.",
237 | "Fransk-prússneska stríðið",
238 | "Franska byltingin",
239 | "Friðrik 1. Danakonungur",
240 | "Friðrik 2. Danakonungur",
241 | "Friðrik 3. Danakonungur",
242 | "Friðrik 4. Danakonungur",
243 | "Friðrik 5. Danakonungur",
244 | "Friðrik barbarossa",
245 | "Friðrik Ólafsson",
246 | "Frostaveturinn mikli",
247 | "Frostaveturinn mikli 1917-18",
248 | "Frumlag",
249 | "Frá vöggu til vöggu",
250 | "Fríða Á. Sigurðardóttir",
251 | "Félagseyjar",
252 | "Fólínsýra",
253 | "Gagnagrunnur",
254 | "Garðabær",
255 | "George Tiller",
256 | "Gerlar",
257 | "Gigt",
258 | "Ginnungagap",
259 | "Gjaldþrot",
260 | "Gland",
261 | "Glaumbær",
262 | "Glúkósi",
263 | "Goodluck Jonathan",
264 | "Gosberg",
265 | "Gotar",
266 | "Greenwich",
267 | "Gregoríska tímatalið",
268 | "Grettir Ásmundarson",
269 | "Grettis saga",
270 | "Grikkland",
271 | "Grikkland hið forna",
272 | "Grund",
273 | "Grábrók",
274 | "Grænserkur",
275 | "Gróðurhúsalofttegund",
276 | "Gröftur",
277 | "Gufunes",
278 | "Gullbringu- og Kjósarsýsla",
279 | "Gunnólfsvíkurfjall",
280 | "Guðmundur Gunnarsson",
281 | "Guðmundur Ólafsson",
282 | "Gísli Pálmi",
283 | "Hafdís Huld Þrastardóttir",
284 | "Hallmundarhraun",
285 | "Han-ættin",
286 | "Hannes Hólmsteinn Gissurarson",
287 | "Hans Danakonungur",
288 | "Hattur",
289 | "Heidi Klum",
290 | "Heimsvaldastefna",
291 | "Heinrich Himmler",
292 | "Heiðabær",
293 | "Heiðar Guðjónsson",
294 | "Hektari",
295 | "Helförin",
296 | "Helga Sigurðardóttir",
297 | "Helgi Björnsson",
298 | "Hellisheiðarvirkjun",
299 | "Helvíti",
300 | "Hernán Cortés",
301 | "Hinrik",
302 | "Hitadeigt plast",
303 | "Hljómar - Ertu með?",
304 | "Hljómsveit Ingimars Eydal - Á sjó",
305 | "Hljóðan",
306 | "Hljóðvarp",
307 | "Hofsós",
308 | "Hormón",
309 | "Hornbjarg",
310 | "Hrafnagil",
311 | "Hraunfossar",
312 | "Hreyfing",
313 | "Hugræn sálfræði",
314 | "Hugsvinnsmál",
315 | "Háhitasvæði",
316 | "Háskóli",
317 | "Hættir sagna í íslensku",
318 | "Hólar í Hjaltadal",
319 | "Hóstarkirtill",
320 | "Höfði",
321 | "IPod touch",
322 | "Immanuel Kant",
323 | "Indverski þjóðarráðsflokkurinn",
324 | "Ingvar E. Sigurðsson",
325 | "Ivanka Trump",
326 | "Jakobsvegurinn",
327 | "Japanska",
328 | "Jarðkeppur",
329 | "Jaspis",
330 | "Javier Milei",
331 | "Jean Charles de Menezes",
332 | "Joe Jonas",
333 | "John Grant",
334 | "Járntjaldið",
335 | "Jóhann Jónsson",
336 | "Jóhann Sigurjónsson",
337 | "JóiPé og Króli",
338 | "Jól",
339 | "Jólakötturinn",
340 | "Jón Gunnar Bernburg",
341 | "Jón lærði Guðmundsson",
342 | "Jón Örn Loðmfjörð",
343 | "Jósef",
344 | "Jökulgarður",
345 | "Kalda stríðið",
346 | "Kapalsjónvarp",
347 | "Kapetingar",
348 | "Karl Ágúst Úlfsson",
349 | "Katla",
350 | "Katrín Jakobsdóttir",
351 | "Kembur",
352 | "Kennsl",
353 | "Kings Canyon-þjóðgarðurinn",
354 | "Kjarnorka",
355 | "Kjósarsýsla",
356 | "Kjörís",
357 | "Klassíski tíminn",
358 | "Kleina",
359 | "Klofning",
360 | "Knattspyrna",
361 | "Knattspyrna á Íslandi",
362 | "Knattspyrnufélag Fjallabyggðar",
363 | "Knattspyrnufélagið Víkingur",
364 | "Kok",
365 | "Kol",
366 | "Kolsýra",
367 | "Koltrefjar",
368 | "Kristján 1.",
369 | "Kristján 2.",
370 | "Kristján 3.",
371 | "Kristján 4.",
372 | "Kristján 5.",
373 | "Kristján 6.",
374 | "Kristján 7.",
375 | "Krossblómaætt",
376 | "Kvensöðull",
377 | "Kvikmynd",
378 | "Kynjalyf",
379 | "Kænugarður",
380 | "Kókaín",
381 | "Kósakkar",
382 | "Körfuknattleikur",
383 | "Landgrunn",
384 | "Landshöfðingi",
385 | "Laufskógar",
386 | "Leikbók",
387 | "Lettland",
388 | "Lissabon-sáttmálinn",
389 | "Listamannadeilan",
390 | "Listi yfir erlendar ferðabækur um Ísland",
391 | "Listi yfir heimspekinga",
392 | "Listi yfir hnúta",
393 | "Listi yfir leiðarstjörnur",
394 | "Listi yfir morð á Íslandi frá 1970–1999",
395 | "Listi yfir páfa",
396 | "Listi yfir vegamálastjóra",
397 | "Listi yfir íslensk eiginnöfn karlmanna",
398 | "Listi yfir íslensk mannanöfn",
399 | "Listi yfir íslensk póstnúmer",
400 | "Listi yfir íslenska tónlistarmenn",
401 | "Listi yfir útvarpsstöðvar á Íslandi",
402 | "Little Rock",
403 | "Ljósmynd",
404 | "Ljótu hálfvitarnir",
405 | "Loftþrýstingur",
406 | "Loki",
407 | "London School of Economics",
408 | "Los Angeles",
409 | "Loðna gullmoldvarpa",
410 | "Lunga",
411 | "Látra-Björg",
412 | "Lén",
413 | "Lífeldsneyti",
414 | "Líftækni",
415 | "Lögskýringarleið",
416 | "MacBook Air",
417 | "Magnús Stephensen",
418 | "Mahatma Gandhi",
419 | "Manchester",
420 | "Mannanafnanefnd",
421 | "Marktækni",
422 | "Markús Árelíus",
423 | "Matteo Ricci",
424 | "Max Weber",
425 | "McGill-háskóli",
426 | "MediaWiki",
427 | "Melasmári",
428 | "Menntaskólinn við Hamrahlíð",
429 | "Menntun",
430 | "Mið-Afríkulýðveldið",
431 | "Mánuður",
432 | "Mæðradagurinn",
433 | "Möttulstrókur",
434 | "Möttulstrókurinn undir Íslandi",
435 | "Múmínálfarnir",
436 | "Mýrarrauði",
437 | "Mýri",
438 | "Napóleon Bónaparte",
439 | "Negull",
440 | "Norður-Maríanaeyjar",
441 | "Nykur",
442 | "Nígeríska karlalandsliðið í knattspyrnu",
443 | "Nýlenduveldi",
444 | "Nýnorska",
445 | "Oddur Gottskálksson",
446 | "Opinn hugbúnaður",
447 | "Oprah Winfrey",
448 | "Orrustan við Dybbøl",
449 | "Otto Wathne",
450 | "Paolo Macchiarini",
451 | "Patreksfjörður",
452 | "Pennsylvanía",
453 | "Philippe Pétain",
454 | "Pinus johannis",
455 | "Plastbarkamálið",
456 | "Pressa",
457 | "Purpuri",
458 | "Páll Bergþórsson",
459 | "Pétur Halldórsson",
460 | "Pétur Haraldsson Blöndal",
461 | "Qalqilya",
462 | "Ragnhildur Gísladóttir",
463 | "Raunvextir",
464 | "Reykjavík",
465 | "Runólfur Ágústsson",
466 | "Rás 1",
467 | "Ríkisútvarpið",
468 | "Rím",
469 | "Ródesía",
470 | "Rómeó og Júlía",
471 | "Rökfræðileg raunhyggja",
472 | "Rúnar Kristinsson",
473 | "Sagnfræði",
474 | "Saltsýra",
475 | "Salvör",
476 | "Samrás",
477 | "Samskipadeild karla í knattspyrnu 1992",
478 | "Sanitas",
479 | "Sanngjörn afnot",
480 | "Savojaættin",
481 | "Senegal",
482 | "Seychelles-eyjar",
483 | "Shijiazhuang",
484 | "Sif Sigmarsdóttir",
485 | "Sigríður Hagalín Björnsdóttir",
486 | "Sigrún Edda Björnsdóttir",
487 | "Sigurður Geirdal",
488 | "Sigurður málari",
489 | "Sigð",
490 | "Sjáaldur",
491 | "Sjálfstæði nýlendnanna",
492 | "Sjávarspendýr",
493 | "Sjóvá-Almennra deild karla í knattspyrnu 1995",
494 | "Sjóvár-Almennra deild karla í knattspyrnu 1996",
495 | "Sjúkraflutningamaður",
496 | "Skapahár",
497 | "Skrifstofustjóri Alþingis",
498 | "Skriðjökull",
499 | "Skyndihjálp",
500 | "Skytturnar þrjár",
501 | "Skálmöld",
502 | "Skötuselur",
503 | "Skúli Magnússon",
504 | "Slot",
505 | "Sprengidagur",
506 | "Spænska",
507 | "Staðaraðferð",
508 | "Stiklusteinabrú",
509 | "Stiklutexti",
510 | "Stríð Prússlands og Austurríkis",
511 | "Stuðlaberg",
512 | "Stóuspeki",
513 | "Stöðurafmagn",
514 | "Stöðuvatn",
515 | "Summarfestivalurin",
516 | "Suzhou",
517 | "Suður-Kaliforníuháskóli",
518 | "Suður-Þingeyjarsýsla",
519 | "Suðurnesjabær",
520 | "Suðvestur-England",
521 | "Svartþröstur",
522 | "Sveitarfélagið Vogar",
523 | "Sálin hans Jóns míns",
524 | "Sæmundur fróði Sigfússon",
525 | "Sódavatn",
526 | "Sókrates",
527 | "Söngvari",
528 | "Súlú",
529 | "Súrefnismettunarmæling",
530 | "Taylor Lautner",
531 | "Taíland",
532 | "Tegund",
533 | "Thymus",
534 | "Tilvistarstefna",
535 | "Tjara",
536 | "Tjóðrun",
537 | "Torfbær",
538 | "Trans Ísland",
539 | "Trópídeild karla í knattspyrnu 1994",
540 | "Tæring",
541 | "Tíbeska hásléttan",
542 | "Tölva",
543 | "Túaregar",
544 | "Túnis",
545 | "USC Annenberg School for Communication",
546 | "Umferðarljós",
547 | "Upphafsstafaheiti",
548 | "Upplýsingin",
549 | "Urumqi",
550 | "Urðir",
551 | "User:Sneeuwschaap",
552 | "Utah",
553 | "Vagn",
554 | "Vegabréf",
555 | "Vegtollur",
556 | "Verðtrygging",
557 | "Vesturbær Reykjavíkur",
558 | "Vinstrihreyfingin – grænt framboð",
559 | "Virkisjökull",
560 | "Virðisaukaskattur",
561 | "Viðtengingarháttur",
562 | "Vísnabók Guðbrands",
563 | "Walter Cronkite",
564 | "Waltzing Matilda",
565 | "Web Content Accessibility Guidelines",
566 | "Wikimedia Commons",
567 | "Yasmina Reza",
568 | "Zug",
569 | "Áburðarverksmiðjan í Gufunesi",
570 | "Áhrifavaldur",
571 | "Árnes",
572 | "Árnessýsla",
573 | "Árni Björnsson",
574 | "Ástralía",
575 | "Ísafjarðardjúp",
576 | "Íslamska ríkið",
577 | "Íslandspóstur",
578 | "Íslenska",
579 | "Íslenska stafrófið",
580 | "Íslensku tónlistarverðlaunin 2008",
581 | "Ítalía",
582 | "Íþróttafélagið Huginn",
583 | "Íþróttafélagið Þór Akureyri",
584 | "Ólífa",
585 | "Ólöf Sigurðardóttir frá Hlöðum",
586 | "Ómar Ingi Magnússon",
587 | "Úkraína",
588 | "Útvarp Saga",
589 | "Útvarpsstjóri",
590 | "Þjóðhöfðingjar Danmerkur",
591 | "Þorskastríðin",
592 | "Þrjár stoðir Evrópusambandsins",
593 | "Þróunarlíffræði",
594 | "Þátttaka Íslands á Ólympíuleikum",
595 | "Þæfing",
596 | "Þíðjökull",
597 | "Þóra Arnórsdóttir"
598 | ]
599 | }
--------------------------------------------------------------------------------
/words/wikipedia/la_wikipedia.json:
--------------------------------------------------------------------------------
1 | {
2 | "words": [
3 | "Carolus Puigdemont",
4 | "Maria Antonia Austriaca",
5 | "Impetus Lutetiae Novembri 2015 facti",
6 | "Lingua Latina",
7 | "Scrotum",
8 | "2023",
9 | "Cursus eventorum quae ad Bellum Civile Americanum adduxerunt",
10 | "Eintracht Frankfurt",
11 | "Henricus Kissinger",
12 | "Iuris consultus",
13 | "Vici",
14 | "Abū Firās al-Ḥamdānī",
15 | "Aegidius",
16 | "Andorra",
17 | "Bracara Augusta",
18 | "Canno",
19 | "Carthago Nova",
20 | "Chlodovechus I",
21 | "Commentarii de Inepto Puero",
22 | "Cultura Minoa",
23 | "Durocortorum",
24 | "Gregorius Magnus",
25 | "Haroldus II",
26 | "Harrison Ford",
27 | "Hyperaldosteronismus",
28 | "Idriss Déby",
29 | "Ientaculum",
30 | "Ioanna Moreau",
31 | "Ioannes Stoltenberg",
32 | "Iosephus Conrad",
33 | "Kemi",
34 | "Latomismus",
35 | "Lingua Francogallica",
36 | "Lingua Nordica antiqua",
37 | "Lucius Iunius Brutus",
38 | "Lucus Augusti",
39 | "Mens",
40 | "Molière",
41 | "Novum Eboracum",
42 | "Opus fictile",
43 | "Orestes",
44 | "Piper nigrum",
45 | "Protomartyres Romani",
46 | "Receptorium NMDA",
47 | "Ricardus Stallman",
48 | "Rigordus",
49 | "Rodica-Michaela Stănoiu",
50 | "Simiiformes",
51 | "Status Pontificius",
52 | "Tau",
53 | "1017 Jacqueline",
54 | "14 Iunii",
55 | "1531",
56 | "1777",
57 | "1815",
58 | "1912",
59 | "1930",
60 | "1941",
61 | "1945",
62 | "2007",
63 | "2 Decembris",
64 | "7596 Yumi",
65 | "8142 Zolotov",
66 | "8 Augusti",
67 | "A",
68 | "Academia Scientiarum Russica",
69 | "Acis Vallis Viridis",
70 | "Adolphus Hitler",
71 | "Aelius Theon",
72 | "Aeneis",
73 | "Aenus",
74 | "Aeschines",
75 | "Ager Uvadensis",
76 | "Ager Vaticanus",
77 | "Agrapha",
78 | "Alchemia",
79 | "Alcides De Gasperi",
80 | "Alexander Alechin",
81 | "Alexander Darling",
82 | "Alpha-synucleinum",
83 | "Altissima planities Tibetana",
84 | "Ambrosius",
85 | "Amstelodanum",
86 | "Angaria",
87 | "Anilingus",
88 | "Anna-Barbara Gerl-Falkovitz",
89 | "Anulorum Erus",
90 | "Architrenius",
91 | "Armenia",
92 | "Ars ingeniaria",
93 | "Asteroidea",
94 | "Atropatene",
95 | "Augusta Treverorum",
96 | "Augustinus",
97 | "Aulus Atilius A.f. Calatinus",
98 | "Aurelianus",
99 | "Baracus Obama",
100 | "Basilica cathedralis Sancti Dionysii",
101 | "Beda",
102 | "Belgium",
103 | "Bellonia",
104 | "Berquilla",
105 | "Bletisa",
106 | "Bootes",
107 | "Bowie",
108 | "Bruce Dickinson",
109 | "Buch am Wald",
110 | "Burgi",
111 | "Butuntum",
112 | "Cafea expressa",
113 | "Calendarium Gregorianum",
114 | "Calesium",
115 | "California",
116 | "Cambosia",
117 | "Canalis",
118 | "Canticum Canticorum",
119 | "Carceres secreti CIA",
120 | "Carelia",
121 | "Carolus Martellus",
122 | "Catalaunia",
123 | "Cathanesiensis",
124 | "Cechia",
125 | "Celticus",
126 | "Champey",
127 | "Charlie hebdo",
128 | "Cilicium",
129 | "Circuitus integratus",
130 | "Circulus Dupont",
131 | "Citroën C4",
132 | "Civitates Foederatae",
133 | "Civitates Foederatae Americae",
134 | "Civitatum Foederatarum Secretarius Civitatis",
135 | "Classis",
136 | "Classis Romana",
137 | "Claudiopolis",
138 | "Cleopatrae mors",
139 | "Cluniacum",
140 | "Codex Iustinianus",
141 | "Codex Theodosianus",
142 | "Colin Farrell",
143 | "Collegium Cardinalium",
144 | "Collegium Gulielmi et Mariae",
145 | "Collegium Romanum",
146 | "Computatrum",
147 | "Connecticuta",
148 | "Corpus Iuris Civilis",
149 | "Corpus iuris canonici",
150 | "Corpus iuris civilis",
151 | "Cowes",
152 | "Cracovia",
153 | "Creep",
154 | "Croneburgum",
155 | "Cruces et circuli",
156 | "Crux gammata",
157 | "Cthulhu",
158 | "Cyprus",
159 | "Datorum repositorium",
160 | "David Livingstone",
161 | "De flagrorum usu in re veneria",
162 | "December",
163 | "Der kleine Pauly",
164 | "Dies Iovis",
165 | "Dioecesis Patavina",
166 | "Diurnariorum ars",
167 | "Diversitas culturae",
168 | "Domus Sabaudiana",
169 | "Draco",
170 | "Drenthia",
171 | "Duala",
172 | "Duces civitatum Europaearum hodiernarum",
173 | "Duglassius Adams",
174 | "Ecclesia Iesu Christi Diebus Ultimis Sanctorum",
175 | "Electricitas statica",
176 | "Elisabetha Taylor",
177 | "Elisaeus",
178 | "Ella Fitzgerald",
179 | "Emerita Augusta",
180 | "Episcopus",
181 | "Erga omnes",
182 | "Ericus Fottorino",
183 | "Eugenius Franciscus Vidocq",
184 | "Eusebius",
185 | "Fada",
186 | "Farina",
187 | "Fatum Manifestum",
188 | "Ferdinandus Cortesius",
189 | "Flavus",
190 | "Flora Sinensis",
191 | "Forum Vetus",
192 | "Franciscus",
193 | "Fretum Magellanicum",
194 | "Fundus Animalium",
195 | "Gaius Plinius Secundus",
196 | "Gazella",
197 | "Geraldina Ferraro",
198 | "Globalizatio",
199 | "Gloria in excelsis",
200 | "Graece",
201 | "Grosne",
202 | "Guichainville",
203 | "Gulielmus von Humboldt",
204 | "Haedui",
205 | "Halbe Zijlstra",
206 | "Hannovera",
207 | "Harry Potter and the Goblet of Fire",
208 | "Harvaroddus",
209 | "Haskala",
210 | "Hasta el fin del mundo",
211 | "Henricus Gulielmus Kopf",
212 | "Herma",
213 | "Hester Lynch Piozzi",
214 | "Hieronymus Lalande",
215 | "Historia",
216 | "Historia Romana",
217 | "Historia rerum in partibus transmarinis gestarum",
218 | "Homininae",
219 | "Hominini",
220 | "Homo",
221 | "Horatii et Curiatii",
222 | "Humphredus Bogart",
223 | "Hymnus nationalis",
224 | "Hypertextus",
225 | "Ianuarii",
226 | "Immobile",
227 | "Imperator Sacuramati",
228 | "Imperatores Iaponiae",
229 | "Imperatrix Go-Sacuramati",
230 | "Imperium Romanum",
231 | "Imperium Romanum Occidentale",
232 | "Imperium Romanum Orientale",
233 | "Impi",
234 | "In girum imus nocte ecce et consumimur igni",
235 | "Inanna",
236 | "Inclavatura et exclavatura",
237 | "Index communium Nederlandiae",
238 | "Indonesia",
239 | "Infernus",
240 | "Infrastructura et superstructura",
241 | "Innocentius IV",
242 | "Inscriptio de sancto Clemente et Sisinio",
243 | "Ioanna Calment",
244 | "Ioannes Barbu",
245 | "Ioannes Lubbock",
246 | "Ioannes Michael Lounge",
247 | "Ioannes Poddubnyj",
248 | "Ioannes Renatus Constantinus Quoy",
249 | "Iosephus Biden",
250 | "Iota",
251 | "Isaac Rabin",
252 | "Isaias de Noronha",
253 | "Isala Hollandica",
254 | "Islandia",
255 | "Iudex",
256 | "Iura civitatum",
257 | "Iura minoritatum",
258 | "Iurisprudentia",
259 | "Ius",
260 | "Ius Romanum",
261 | "Ius civile",
262 | "Ius cogens",
263 | "Ius gentium",
264 | "Ius in rem",
265 | "Ius naturale",
266 | "Ius privatum",
267 | "Iustinianus I",
268 | "Jelle Zijlstra",
269 | "Jonchery",
270 | "Kanye West",
271 | "Kenethus Annakin",
272 | "Kiyomizu-dera",
273 | "Kyrgyzstania",
274 | "LZ 129 Hindenburg",
275 | "Legio XV Apollinaris",
276 | "Lex",
277 | "Lex militaris",
278 | "Liber",
279 | "Libera res publica Romana",
280 | "Lingua Atropatenica",
281 | "Lingua Hispanica",
282 | "Lingua Iaponica",
283 | "Lingua Palica",
284 | "Lingua Urdu",
285 | "Lingua Vietnamensis",
286 | "Linguae Lahnda",
287 | "Linguae Uto-Aztecae",
288 | "Liquor",
289 | "Luchianum",
290 | "Ludi Saeculares",
291 | "Ludovicus XVIII",
292 | "Mancunium",
293 | "Manifestationes Aegyptiae anni 2011",
294 | "Manzanum",
295 | "Marcus Hunter",
296 | "Martinica",
297 | "Materia medica",
298 | "Matterhorn",
299 | "Maximilianus Weber",
300 | "Maximimum",
301 | "Medicina succursoria",
302 | "Meland",
303 | "Menno Simons",
304 | "Mia Farrow",
305 | "Microsoft Windows",
306 | "Milovan Đilas",
307 | "Motus",
308 | "My",
309 | "Nancianum",
310 | "Naturalis historia",
311 | "Naufragium",
312 | "Navis",
313 | "Navis Nemorensis",
314 | "Neapolis",
315 | "Neutron",
316 | "Nicolaus Harvey",
317 | "Nicolaus Kuznecov",
318 | "Nissa",
319 | "NoHo",
320 | "Nova Hibernia",
321 | "Oceanus Germanicus",
322 | "Oliverus Sacks",
323 | "Omicron",
324 | "Onomatopoeia",
325 | "Oppidum",
326 | "Orbita",
327 | "Oxoniensis comitatus",
328 | "Oxycoccus",
329 | "Pafyumu",
330 | "Pagina prima",
331 | "Pan",
332 | "Pandectae",
333 | "Pelagius II",
334 | "Pendulum",
335 | "Penis hominis",
336 | "Petasus",
337 | "Petrus Lellouche",
338 | "Philolaus Crotoniensis",
339 | "Photographema",
340 | "Physalis peruviana",
341 | "Pitcairn Insulae",
342 | "Polynices",
343 | "Pons",
344 | "Positivismus logicus",
345 | "Possidius Calamensis",
346 | "Praesumptio iuris tantum",
347 | "Praga",
348 | "Primates",
349 | "Proelium",
350 | "Programma fontium apertorum",
351 | "Propulsorium",
352 | "Prudentius",
353 | "Psychologia",
354 | "Pulchritudo",
355 | "Purpura",
356 | "Q",
357 | "Reformatio",
358 | "Regio",
359 | "Res Publica Utriusque Nationis",
360 | "Rhenania Septentrionalis-Vestfalia",
361 | "Rho",
362 | "Ricardus David Precht",
363 | "Robertus Einstein",
364 | "Robertus Franciscus Kennedy",
365 | "Rogerius Scruton",
366 | "Rudolphus Höß",
367 | "Ruthenia Alba",
368 | "S",
369 | "Sabinus a Placentia",
370 | "Saeculum 18",
371 | "Saeculum 19",
372 | "Sally Margarita Field",
373 | "Salome",
374 | "Samara",
375 | "Samuel Beckett",
376 | "Sarah Kerrigan",
377 | "Schola Genevensis",
378 | "Sculptura",
379 | "Secundum bellum mundanum",
380 | "Semen",
381 | "Sociologia",
382 | "Somnium",
383 | "Sorbona",
384 | "Spartacus",
385 | "Speusippus",
386 | "Statio Terminalis Media Grandis",
387 | "Sublimis",
388 | "Sui iuris",
389 | "Suppositorium",
390 | "Surria",
391 | "Tavaux",
392 | "Technologia",
393 | "Terminator: Genisys",
394 | "Terrae australes antarcticaeque Francicae",
395 | "The Call of Cthulhu",
396 | "The Dark Knight",
397 | "The Third Man",
398 | "Theorema binomiale",
399 | "Thulay",
400 | "Tiberius",
401 | "Tibetum",
402 | "Tolonium",
403 | "Tournefeuille",
404 | "Transitus Urigae",
405 | "Tugium",
406 | "Tyrtarion",
407 | "UTC+02:00",
408 | "Ucraina",
409 | "Unio Sovietica",
410 | "United States Department of State",
411 | "Universitas Bonaëropolitana",
412 | "Universitas Californiae Meridianae",
413 | "Universitas Oldenburgensis",
414 | "Valdoie",
415 | "Vallis Dominorum et Vallis Comitum",
416 | "Vascus Gama",
417 | "Vera Crux",
418 | "Vexillum Finniae",
419 | "Vexillum Ucrainae",
420 | "Via lactea",
421 | "Vicifons",
422 | "Vicimedia Communia",
423 | "Vosiolum",
424 | "Vulgata Xystina-Clementina",
425 | "Vulva",
426 | "Woody Guthrie",
427 | "Xi",
428 | "YouTube",
429 | "Ypsilon",
430 | "Zingari",
431 | "theorema binomiale",
432 | "vicipaedia:pagina prima"
433 | ]
434 | }
--------------------------------------------------------------------------------