diff --git a/extras/forms-inputs/dense.css b/extras/forms-inputs/compact.css similarity index 100% rename from extras/forms-inputs/dense.css rename to extras/forms-inputs/compact.css diff --git a/extras/theme-animation.css b/extras/theme-animation.css index 58ead7e..1d62494 100644 --- a/extras/theme-animation.css +++ b/extras/theme-animation.css @@ -50,7 +50,7 @@ body[data-color-transition-enabled] :is( /* BG and Border */ body[data-color-transition-enabled] :is( [data-bg-transition][data-border-transition]:not([data-color-transition]), - #root > footer, + body > footer, p.error-box, .popup form, #outline diff --git a/extras/typography/dense.css b/extras/typography/compact.css similarity index 100% rename from extras/typography/dense.css rename to extras/typography/compact.css diff --git a/package-lock.json b/package-lock.json index 6356dec..1466d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "dependencies": { "@doc-utils/color-themes": "^0.1.14", "@doc-utils/jsonschema2markdown": "^0.1.1", - "@doc-utils/markdown2html": "^0.1.21", + "@doc-utils/markdown2html": "^0.2.1", "glob": "^10.2.3", "luxon": "^3.3.0", "mustache": "^4.2.0", + "xmlbuilder2": "^3.1.1", "yaml": "^2.2.2" }, "bin": { @@ -47,15 +48,15 @@ } }, "node_modules/@doc-utils/markdown2html": { - "version": "0.1.21", - "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.21/markdown2html-0.1.21.tgz", - "integrity": "sha512-yoyWrOOm4NNvxvzGn0oFwGgJb8xwmdQcAzgvN2s+xIC2sVL1WBirOlM+iJ1pOhXfAiKEPqzgEim+SE74siWp9Q==", + "version": "0.2.6", + "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.2.6/markdown2html-0.2.6.tgz", + "integrity": "sha512-6cQNzthYOOlkT6rr6E1lpSJ4Zq991+chblSRxv69SVT8y5cIs6c9tFu75MXoIKX4+H68Q4i10gtMrrsXD+bkaA==", "dependencies": { "bytefield-svg": "^1.6.1", "dompurify": "^2.3.6", "jsdom": "^20.0.1", "katex": "^0.16.7", - "marked": "^4.1.1", + "marked": "^5.0.2", "nomnoml": "^1.5.2", "pikchr": "^0.0.5", "prismjs": "^1.29.0", @@ -80,6 +81,50 @@ "node": ">=12" } }, + "node_modules/@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "engines": { + "node": ">=8.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -131,9 +176,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.16.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz", - "integrity": "sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==", + "version": "18.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz", + "integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==", "dev": true }, "node_modules/@types/prismjs": { @@ -214,6 +259,14 @@ "node": ">=4" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -939,14 +992,14 @@ } }, "node_modules/glob": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz", - "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.5.tgz", + "integrity": "sha512-Gj+dFYPZ5hc5dazjXzB0iHg2jKWJZYMjITXYPBRQ/xc2Buw7H0BINknRTwURJ6IC6MEFpYbLvtgVb3qD+DwyuA==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.0", - "minipass": "^5.0.0", + "minipass": "^5.0.0 || ^6.0.2", "path-scurry": "^1.7.0" }, "bin": { @@ -1062,6 +1115,18 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -1171,14 +1236,14 @@ } }, "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 12" + "node": ">= 18" } }, "node_modules/mime-db": { @@ -1215,11 +1280,11 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", + "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -1274,11 +1339,12 @@ } }, "node_modules/nomnoml": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nomnoml/-/nomnoml-1.5.3.tgz", - "integrity": "sha512-2LBKi3ygeYAC9l/wowgtBRxvQGuTryIczULVk2rFbWGUvs5aX68/sH+WgL95CschnSnxucTIv9L+3xScS71obQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/nomnoml/-/nomnoml-1.6.0.tgz", + "integrity": "sha512-+t3ZBx/yBBnvzQkVU0/pyXZNGHIyRdnPBg/8H7YvWBbvRroRG21amC2kZ1UVhGes2kU2cU6KjO4RSDgglajeQw==", "dependencies": { - "graphre": "^0.1.3" + "graphre": "^0.1.3", + "nomnoml": "^1.5.3" }, "bin": { "nomnoml": "dist/nomnoml-cli.js" @@ -1366,12 +1432,12 @@ } }, "node_modules/path-scurry": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz", - "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz", + "integrity": "sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==", "dependencies": { "lru-cache": "^9.1.1", - "minipass": "^5.0.0" + "minipass": "^5.0.0 || ^6.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1544,6 +1610,11 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2360,6 +2431,20 @@ "node": ">=12" } }, + "node_modules/xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "dependencies": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -2469,15 +2554,15 @@ } }, "@doc-utils/markdown2html": { - "version": "0.1.21", - "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.21/markdown2html-0.1.21.tgz", - "integrity": "sha512-yoyWrOOm4NNvxvzGn0oFwGgJb8xwmdQcAzgvN2s+xIC2sVL1WBirOlM+iJ1pOhXfAiKEPqzgEim+SE74siWp9Q==", + "version": "0.2.6", + "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.2.6/markdown2html-0.2.6.tgz", + "integrity": "sha512-6cQNzthYOOlkT6rr6E1lpSJ4Zq991+chblSRxv69SVT8y5cIs6c9tFu75MXoIKX4+H68Q4i10gtMrrsXD+bkaA==", "requires": { "bytefield-svg": "^1.6.1", "dompurify": "^2.3.6", "jsdom": "^20.0.1", "katex": "^0.16.7", - "marked": "^4.1.1", + "marked": "^5.0.2", "nomnoml": "^1.5.2", "pikchr": "^0.0.5", "prismjs": "^1.29.0", @@ -2499,6 +2584,38 @@ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, + "@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "requires": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "requires": { + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "requires": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==" + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2544,9 +2661,9 @@ "dev": true }, "@types/node": { - "version": "18.16.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz", - "integrity": "sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==", + "version": "18.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz", + "integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==", "dev": true }, "@types/prismjs": { @@ -2606,6 +2723,14 @@ "color-convert": "^1.9.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -3134,14 +3259,14 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "glob": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz", - "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.5.tgz", + "integrity": "sha512-Gj+dFYPZ5hc5dazjXzB0iHg2jKWJZYMjITXYPBRQ/xc2Buw7H0BINknRTwURJ6IC6MEFpYbLvtgVb3qD+DwyuA==", "requires": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.0", - "minipass": "^5.0.0", + "minipass": "^5.0.0 || ^6.0.2", "path-scurry": "^1.7.0" } }, @@ -3219,6 +3344,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -3298,9 +3432,9 @@ "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==" }, "marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==" }, "mime-db": { "version": "1.52.0", @@ -3324,9 +3458,9 @@ } }, "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", + "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==" }, "ms": { "version": "2.1.2", @@ -3368,11 +3502,12 @@ } }, "nomnoml": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nomnoml/-/nomnoml-1.5.3.tgz", - "integrity": "sha512-2LBKi3ygeYAC9l/wowgtBRxvQGuTryIczULVk2rFbWGUvs5aX68/sH+WgL95CschnSnxucTIv9L+3xScS71obQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/nomnoml/-/nomnoml-1.6.0.tgz", + "integrity": "sha512-+t3ZBx/yBBnvzQkVU0/pyXZNGHIyRdnPBg/8H7YvWBbvRroRG21amC2kZ1UVhGes2kU2cU6KjO4RSDgglajeQw==", "requires": { - "graphre": "^0.1.3" + "graphre": "^0.1.3", + "nomnoml": "^1.5.3" } }, "nwsapi": { @@ -3433,12 +3568,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-scurry": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz", - "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz", + "integrity": "sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==", "requires": { "lru-cache": "^9.1.1", - "minipass": "^5.0.0" + "minipass": "^5.0.0 || ^6.0.2" } }, "pikchr": { @@ -3562,6 +3697,11 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "optional": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4239,6 +4379,17 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" }, + "xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "requires": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + } + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 23fce79..951a5fa 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,11 @@ "dependencies": { "@doc-utils/color-themes": "^0.1.14", "@doc-utils/jsonschema2markdown": "^0.1.1", - "@doc-utils/markdown2html": "^0.1.21", + "@doc-utils/markdown2html": "^0.2.1", "glob": "^10.2.3", "luxon": "^3.3.0", "mustache": "^4.2.0", + "xmlbuilder2": "^3.1.1", "yaml": "^2.2.2" } } diff --git a/src/bin/docs2website.ts b/src/bin/docs2website.ts index 57a3e00..58c27dd 100644 --- a/src/bin/docs2website.ts +++ b/src/bin/docs2website.ts @@ -1,6 +1,6 @@ import { read_config } from '../conf'; -import { build_docs_project } from '../build'; +import { build_docs_project } from '../build-files'; main(); diff --git a/src/build-files/helpers.ts b/src/build-files/helpers.ts new file mode 100644 index 0000000..2c75a68 --- /dev/null +++ b/src/build-files/helpers.ts @@ -0,0 +1,230 @@ + +import { dirname, join as path_join } from 'path'; +import { mkdirp, write_text } from '../fs'; +import { icons } from '../icons'; +import { BuildState } from './state'; +import { load_partials, FrontMatter, Context, load_layout, render_template } from '../template'; +import { render_theme_css_properties } from '../themes'; +import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html'; +import { CalendarConfig, RSSConfig } from '../conf'; + +export interface OutFileURL { + base_url: string; + rel_path: string; + dir_url: string; + abs_url: string; +} + +export function map_output_file_to_url(state: BuildState, out_file: string, index_file?: string) : OutFileURL { + let rel_path = out_file.slice(state.conf.input.root.length); + + if (index_file && rel_path.endsWith('/' + index_file)) { + rel_path = rel_path.slice(0, -(index_file.length + 1)); + } + + if (! rel_path.startsWith('/')) { + rel_path = '/' + rel_path; + } + + const base_url = state.conf.base_url.endsWith('/') + ? state.conf.base_url.slice(0, -1) + : state.conf.base_url; + + return { + base_url, + rel_path, + dir_url: base_url + dirname(rel_path), + abs_url: base_url + rel_path + }; +} + +export async function map_input_file_to_output_file(state: BuildState, in_file: string, remove_exts?: string[], add_ext?: string) { + if (! in_file.startsWith(state.conf.input.root)) { + throw new Error('input file expected to be inside input root'); + } + + let out_file = path_join(state.conf.output.root, in_file.slice(state.conf.input.root.length)); + + if (remove_exts) { + for (const ext of remove_exts) { + if (out_file.endsWith(ext)) { + out_file = out_file.slice(0, -ext.length); + break; + } + } + } + + if (add_ext) { + out_file += add_ext; + } + + const dir = dirname(out_file); + + if (! state.made_directories.has(dir)) { + state.made_directories.add(dir); + await mkdirp(dir); + } + + return out_file; +} + +export async function build_partials(state: BuildState) { + state.partials = await load_partials(state.conf); + + for (const [name, theme] of Object.entries(state.themes)) { + state.partials[`.themes/${name}`] = render_theme_css_properties(theme); + } + + Object.assign(state.partials, state.extras); +} + +export function mustache_context(state: BuildState, page_url: string, frontmatter?: FrontMatter) : Context { + return { + env: state.env, + page: frontmatter, + base_url: state.conf.base_url, + page_url: page_url, + build_time: state.build_time, + icons: icons, + themes: Object.values(state.themes), + theme_groups: structuredClone(state.theme_groups), + markdown: { + render_inline() { + return (text, render) => { + const md = render(text) + const html = render_markdown_to_html_inline_sync(md, + Object.assign({ }, state.conf.markdown, { base_url: page_url }) + ); + return html; + }; + }, + } + }; +} + +export async function render_page(state: BuildState, out_file: string, out_url: OutFileURL, text: string, render_as_markdown: boolean, frontmatter?: any) { + if (render_as_markdown) { + const opts = Object.assign({ }, state.conf.markdown, { + base_url: out_url.abs_url + }); + + text = await render_markdown_to_html(text, opts); + } + + let layout: string; + const layout_file = frontmatter?.layout; + + if (layout_file) { + if (! state.layouts[layout_file]) { + state.layouts[layout_file] = await load_layout(state.conf, layout_file); + } + + layout = state.layouts[layout_file]; + } + + const tags = state.conf.templates?.tags; + const context = mustache_context(state, out_url.abs_url, frontmatter); + const rendered = render_template(text, context, layout, structuredClone(state.partials), tags); + await write_text(out_file, rendered); + + handle_page_side_effects(state, out_file, out_url, frontmatter); +} + +function handle_page_side_effects(state: BuildState, out_file: string, out_url: OutFileURL, frontmatter?: any) { + // Only actual HTML webpages are registered with things like the RSS feed; This is + // to prevent things like CSS files showing up, which may be templated (and therefore + // pass through this function), but are not really "pages" + if (frontmatter && typeof frontmatter === 'object' && out_file.endsWith('.html')) { + if (state.conf.rss) { + if (Array.isArray(state.conf.rss)) { + for (const rss_conf of state.conf.rss) { + handle_rss(state, rss_conf, out_url, frontmatter); + } + } + + else { + handle_rss(state, { }, out_url, frontmatter); + } + } + + handle_sitemap(state, out_url, frontmatter); + handle_event(state, out_url, frontmatter); + + if (state.conf.calendars) { + for (const cal_conf of state.conf.calendars) { + handle_calendar(state, cal_conf, out_url, frontmatter); + } + } + } +} + +function handle_rss(state: BuildState, rss_conf: RSSConfig, out_url: OutFileURL, frontmatter: any) { + // const field = rss_conf + // const rss_frontmatter = state. + // // +} + +function handle_sitemap(state: BuildState, out_url: OutFileURL, frontmatter: any) { + if (! state.conf.sitemap) { + return; + } + + const sitemap_frontmatter = state.conf.sitemap.front_matter_field + ? frontmatter?.[state.conf.sitemap.front_matter_field] || { } + : frontmatter?.sitemap || { }; + + state.sitemap.push({ + url: out_url.abs_url, + lastmod: state.build_time.iso, + change_freq: sitemap_frontmatter?.change_freq, + priority: sitemap_frontmatter?.priority, + }); +} + +function handle_event(state: BuildState, out_url: OutFileURL, frontmatter: any) { + // +} + +function handle_calendar(state: BuildState, cal_conf: CalendarConfig, out_url: OutFileURL, frontmatter: any) { + // +} + +export function file_hash_matches(state: BuildState, in_file: string, new_hash: string) { + const { old_metadata, new_metadata } = state; + + if (! old_metadata.last_build?.config_hash) { + return false; + } + + if (old_metadata.last_build.config_hash !== new_metadata.last_build.config_hash) { + return false; + } + + in_file = in_file.slice(state.conf.input.root.length); + const old_hash = state.old_metadata.files[in_file]?.last_build_hash; + + if (! old_hash) { + return false; + } + + if (old_hash !== new_hash) { + return false; + } + + return true; +} + +export function skip_file(state: BuildState, in_file: string, out_file: string, out_url: OutFileURL, frontmatter?: any) { + in_file = in_file.slice(state.conf.input.root.length); + state.new_metadata.files[in_file] = structuredClone(state.old_metadata?.files?.[in_file]); + handle_page_side_effects(state, out_file, out_url, frontmatter); +} + +export function update_metadata(state: BuildState, in_file: string, hash: string) { + in_file = in_file.slice(state.conf.input.root.length); + state.new_metadata.files[in_file] = { + first_seen_time: state.old_metadata?.files?.[in_file]?.first_seen_time || state.build_time.iso, + last_build_hash: hash, + last_updated_time: state.build_time.iso, + }; +} diff --git a/src/build-files/index.ts b/src/build-files/index.ts new file mode 100644 index 0000000..7dedf7e --- /dev/null +++ b/src/build-files/index.ts @@ -0,0 +1,96 @@ + +import { DateTime } from 'luxon'; +import { Config } from '../conf'; +import { build_env_scope } from '../env'; +import { load_themes } from '../themes'; +import { load_extras } from '../template'; +import { Metadata } from '../metadata'; +import { hash_obj } from '../hash'; +import { BuildState } from './state'; +import { read_json, with_default_if_missing, write_json } from '../fs'; + +import { copy_raw_files } from './raw'; +import { render_text_file_templates } from './mustache'; +import { render_markdown_files } from './markdown'; +import { render_json_schema_files } from './jsonschema'; +import { write_sitemap_if_needed } from './sitemap'; + +export { BuildState, ThemeGroups } from './state'; + +export async function build_docs_project(conf: Config) { + const now = DateTime.now(); + const conf_hash = hash_obj(conf); + const themes = await load_themes(conf); + const get_metadata = read_json(conf.metadata, false).then(({ parsed }) => parsed); + const metadata = await with_default_if_missing(get_metadata, { + last_build: null, + files: { }, + }); + + const state: BuildState = { + conf, + seen_files: new Set(), + env: build_env_scope(conf), + layouts: Object.create(null), + themes: themes, + theme_groups: { + all: Object.values(themes), + light: [ ], + dark: [ ], + high_contrast: [ ], + low_contrast: [ ], + monochrome: [ ], + greyscale: [ ], + protanopia_safe: [ ], + deuteranopia_safe: [ ], + tritanopia_safe: [ ], + }, + old_metadata: metadata, + new_metadata: { + last_build: { + time: now.toISO(), + config_hash: conf_hash, + }, + files: { } + }, + extras: await load_extras(), + made_directories: new Set(), + sitemap: [ ], + build_time: { + iso: now.toISO(), + rfc2822: now.toRFC2822(), + }, + }; + + for (const theme of Object.values(themes)) { + for (const label of theme.labels) { + state.theme_groups[label].push(theme); + } + } + + if (conf.input.raw) { + await copy_raw_files(state); + } + + if (conf.input.text) { + await render_text_file_templates(state); + } + + if (conf.input.markdown) { + await render_markdown_files(state); + } + + if (conf.input['schema+json'] || conf.input['schema+yaml']) { + await render_json_schema_files(state); + } + + // todo: other file types... + + await write_sitemap_if_needed(state); + // todo: rss + // todo: events + + // Write the updated metadata file + await write_json(conf.metadata, state.new_metadata, true); +} + diff --git a/src/build-files/jsonschema.ts b/src/build-files/jsonschema.ts new file mode 100644 index 0000000..60cd889 --- /dev/null +++ b/src/build-files/jsonschema.ts @@ -0,0 +1,136 @@ + +import { glob } from 'glob'; +import { BuildState } from './state'; +import { read_json, write_text, read_yaml } from '../fs'; +import { build_markdown_from_json_schema } from '@doc-utils/jsonschema2markdown'; +import { stringify as to_yaml } from 'yaml'; +import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers'; + +export async function render_json_schema_files(state: BuildState) { + const promises: Promise[] = [ ]; + const json_files = await glob(state.conf.input['schema+json'], { + absolute: true, + cwd: state.conf.input.root, + }); + const yaml_files = await glob(state.conf.input['schema+yaml'], { + absolute: true, + cwd: state.conf.input.root, + }); + + if (! state.partials) { + await build_partials(state); + } + + for (const in_file of json_files) { + if (state.seen_files.has(in_file)) { + continue; + } + + state.seen_files.add(in_file); + + promises.push( + handle_json_schema_json_file(state, in_file) + ); + } + + for (const in_file of yaml_files) { + if (state.seen_files.has(in_file)) { + continue; + } + + state.seen_files.add(in_file); + + promises.push( + handle_json_schema_yaml_file(state, in_file) + ); + } + + await Promise.all(promises); +} + +export async function handle_json_schema_json_file(state: BuildState, in_file: string) { + const promises: Promise[] = [ ]; + + const out_file = map_input_file_to_output_file(state, in_file, [ '.json' ], '.html'); + const { frontmatter, parsed, json, hash } = await read_json(in_file, true); + + promises.push( + render_json_schema(state, parsed, in_file, await out_file, hash, frontmatter) + ); + + if (state.conf.output.include_inputs?.includes('schema+json')) { + const json_file = await map_input_file_to_output_file(state, in_file); + + promises.push( + write_text(json_file, json) + ); + + if (state.conf.output.include_yaml_and_json) { + const yaml_file = await map_input_file_to_output_file(state, in_file, [ '.json' ], '.yaml'); + + promises.push( + write_text(yaml_file, to_yaml(parsed)) + ); + } + } + + await Promise.all(promises); +} + +export async function handle_json_schema_yaml_file(state: BuildState, in_file: string) { + const promises: Promise[] = [ ]; + + const out_file = map_input_file_to_output_file(state, in_file, [ '.yaml', '.yml' ], '.html'); + const { frontmatter, parsed, yaml, hash } = await read_yaml(in_file, true); + + promises.push( + render_json_schema(state, parsed, in_file, await out_file, hash, frontmatter) + ); + + if (state.conf.output.include_inputs?.includes('schema+yaml')) { + const yaml_file = await map_input_file_to_output_file(state, in_file); + + promises.push( + write_text(yaml_file, yaml) + ); + + if (state.conf.output.include_yaml_and_json) { + const json_file = await map_input_file_to_output_file(state, in_file, [ '.yaml', '.yml' ], '.json'); + + promises.push( + write_text(json_file, JSON.stringify(parsed, null, ' ')) + ); + } + } + + await Promise.all(promises); +} + +export async function render_json_schema(state: BuildState, schema: unknown, in_file: string, out_file: string, hash: string, frontmatter?: any) { + if (frontmatter?.skip) { + return; + } + + const out_url = map_output_file_to_url(state, out_file); + + if (file_hash_matches(state, in_file, hash)) { + return skip_file(state, in_file, out_file, out_url, frontmatter); + } + + const promises: Promise[] = [ ]; + const markdown = build_markdown_from_json_schema(schema); + + if (state.conf.output.include_intermediate_markdown) { + promises.push( + map_input_file_to_output_file(state, out_file, [ '.html' ], '.md') + .then((md_file) => write_text(md_file, markdown)) + ); + } + + promises.push( + render_page(state, out_file, out_url, markdown, true, frontmatter) + ); + + await Promise.all(promises); + update_metadata(state, in_file, hash); +} diff --git a/src/build-files/markdown.ts b/src/build-files/markdown.ts new file mode 100644 index 0000000..09455fd --- /dev/null +++ b/src/build-files/markdown.ts @@ -0,0 +1,48 @@ + +import { glob } from 'glob'; +import { read_text } from '../fs'; +import { BuildState } from './state'; +import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers'; + +export async function render_markdown_files(state: BuildState) { + const promises: Promise[] = [ ]; + const files = await glob(state.conf.input.markdown, { + absolute: true, + cwd: state.conf.input.root, + }); + + if (! state.partials) { + await build_partials(state); + } + + for (const in_file of files) { + if (state.seen_files.has(in_file)) { + continue; + } + + state.seen_files.add(in_file); + + promises.push( + render_markdown_file(state, in_file) + ); + } + + await Promise.all(promises); +} + +export async function render_markdown_file(state: BuildState, in_file: string) { + const out_file = await map_input_file_to_output_file(state, in_file, [ '.md', '.markdown' ], '.html'); + const out_url = map_output_file_to_url(state, out_file); + const { frontmatter, text, hash } = await read_text(in_file); + + if (frontmatter?.skip) { + return; + } + + if (file_hash_matches(state, in_file, hash)) { + return skip_file(state, in_file, out_file, out_url, frontmatter); + } + + await render_page(state, out_file, out_url, text, true, frontmatter); + update_metadata(state, in_file, hash); +} diff --git a/src/build-files/mustache.ts b/src/build-files/mustache.ts new file mode 100644 index 0000000..bb94c6a --- /dev/null +++ b/src/build-files/mustache.ts @@ -0,0 +1,48 @@ + +import { glob } from 'glob'; +import { read_text } from '../fs'; +import { BuildState } from './state'; +import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers'; + +export async function render_text_file_templates(state: BuildState) { + const promises: Promise[] = [ ]; + const files = await glob(state.conf.input.text, { + absolute: true, + cwd: state.conf.input.root, + }); + + if (! state.partials) { + await build_partials(state); + } + + for (const in_file of files) { + if (state.seen_files.has(in_file)) { + continue; + } + + state.seen_files.add(in_file); + + promises.push( + render_text_file_template(state, in_file) + ); + } + + await Promise.all(promises); +} + +export async function render_text_file_template(state: BuildState, in_file: string) { + const out_file = await map_input_file_to_output_file(state, in_file); + const out_url = map_output_file_to_url(state, out_file); + const { frontmatter, text, hash } = await read_text(in_file); + + if (frontmatter?.skip) { + return; + } + + if (file_hash_matches(state, in_file, hash)) { + return skip_file(state, in_file, out_file, out_url, frontmatter); + } + + await render_page(state, out_file, out_url, text, false, frontmatter); + update_metadata(state, in_file, hash); +} diff --git a/src/build-files/raw.ts b/src/build-files/raw.ts new file mode 100644 index 0000000..960a8ca --- /dev/null +++ b/src/build-files/raw.ts @@ -0,0 +1,32 @@ + +import { glob } from 'glob'; +import { BuildState } from './state'; +import { promises as fs } from 'fs'; +import { map_input_file_to_output_file } from './helpers'; + +export async function copy_raw_files(state: BuildState) { + const promises: Promise[] = [ ]; + const files = await glob(state.conf.input.raw, { + absolute: true, + cwd: state.conf.input.root, + }); + + for (const in_file of files) { + if (state.seen_files.has(in_file)) { + continue; + } + + state.seen_files.add(in_file); + + const out_file = map_input_file_to_output_file(state, in_file); + + // todo: check hashes to see if we can skip + + promises.push( + fs.copyFile(in_file, await out_file) + ); + } + + await Promise.all(promises); + // todo: update metadata +} diff --git a/src/build-files/sitemap.ts b/src/build-files/sitemap.ts new file mode 100644 index 0000000..87f0606 --- /dev/null +++ b/src/build-files/sitemap.ts @@ -0,0 +1,57 @@ + +import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; +import { create as create_xml } from 'xmlbuilder2'; +import { BuildState } from './state'; +import { write_text } from '../fs'; +import { resolve as path_resolve } from 'path'; + +export async function write_sitemap_if_needed(state: BuildState) { + if (! state.conf.sitemap) { + return; + } + + const doc = create_xml({ version: '1.0', encoding: 'UTF-8' }); + const urlset = doc.ele('urlset', { + 'xmlns': 'http://www.sitemaps.org/schemas/sitemap/0.9', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd' + }); + + for (const entry of state.sitemap) { + url_elem(urlset, entry.url, entry.lastmod, entry.change_freq, entry.priority); + } + + const out_file = state.conf.sitemap.out_file || path_resolve(state.conf.output.root, 'sitemap.xml'); + const xml = doc.toString({ + indent: ' ', + prettyPrint: true, + }); + + await write_text(out_file, xml); +} + +export interface SitemapEntry { + url: string; + lastmod: string; + change_freq?: ChangeFreq; + priority?: number; +} + +export type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; + +function url_elem(urlset: XMLBuilder, loc: string, lastmod: string, change_freq?: ChangeFreq, priority?: number) { + const url = urlset.ele('url'); + + url.ele('loc').txt(loc); + url.ele('lastmod').txt(lastmod); + + if (change_freq) { + url.ele('changefreq').txt(change_freq); + } + + if (priority != null) { + url.ele('priority').txt(priority.toFixed(1)); + } + + return url; +} diff --git a/src/build-files/state.ts b/src/build-files/state.ts new file mode 100644 index 0000000..9e8943d --- /dev/null +++ b/src/build-files/state.ts @@ -0,0 +1,42 @@ + +import type { Config } from '../conf'; +import type { Metadata } from '../metadata'; +import type { ColorTheme } from '@doc-utils/color-themes'; +import type { SitemapEntry } from './sitemap'; + +export interface BuildState { + conf: Config; + seen_files: Set; + env: Record; + partials?: Record; + layouts: Record; + themes: Record; + theme_groups: ThemeGroups; + extras: Record; + made_directories: Set; + old_metadata: Metadata; + new_metadata: Metadata; + // rss: { + // url: string; + // last_updated: string; + // }[]; + sitemap: SitemapEntry[]; + // events: EventEntry[]; + build_time: { + iso: string; + rfc2822: string; + }; +} + +export interface ThemeGroups { + all: ColorTheme[]; + light: ColorTheme[]; + dark: ColorTheme[]; + high_contrast: ColorTheme[]; + low_contrast: ColorTheme[]; + monochrome: ColorTheme[]; + greyscale: ColorTheme[]; + protanopia_safe: ColorTheme[]; + deuteranopia_safe: ColorTheme[]; + tritanopia_safe: ColorTheme[]; +} diff --git a/src/build.ts b/src/build.ts deleted file mode 100644 index d053b07..0000000 --- a/src/build.ts +++ /dev/null @@ -1,425 +0,0 @@ - -import { ColorTheme } from '@doc-utils/color-themes'; -import { build_markdown_from_json_schema } from '@doc-utils/jsonschema2markdown'; -import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html'; - -import { glob } from 'glob'; -import { DateTime } from 'luxon'; -import { stringify as to_yaml } from 'yaml'; - -import { promises as fs } from 'fs'; -import { dirname, join as path_join } from 'path'; - -import { icons } from './icons'; -import { Config } from './conf'; -import { build_env_scope } from './env'; -import { load_themes, render_theme_css_properties } from './themes'; -import { mkdirp, read_json, read_text, read_yaml, write_text } from './fs'; -import { load_layout, Context, render_template, load_partials, load_extras, FrontMatter } from './template'; - -interface BuildState { - conf: Config; - seen_files: Set; - env: Record; - partials?: Record; - layouts: Record; - themes: Record; - theme_groups: ThemeGroups; - extras: Record; - made_directories: Set; - build_time: { - iso: string; - rfc2822: string; - }; -} - -export interface ThemeGroups { - all: ColorTheme[]; - light: ColorTheme[]; - dark: ColorTheme[]; - high_contrast: ColorTheme[]; - low_contrast: ColorTheme[]; - monochrome: ColorTheme[]; - greyscale: ColorTheme[]; - protanopia_safe: ColorTheme[]; - deuteranopia_safe: ColorTheme[]; - tritanopia_safe: ColorTheme[]; -} - -export async function build_docs_project(conf: Config) { - const now = DateTime.now(); - const themes = await load_themes(conf); - const state: BuildState = { - conf, - seen_files: new Set(), - env: build_env_scope(conf), - layouts: Object.create(null), - themes: themes, - theme_groups: { - // fixme: this is horribly inefficient - all: Object.values(themes), - light: Object.values(themes).filter((theme) => theme.labels.includes('light')), - dark: Object.values(themes).filter((theme) => theme.labels.includes('dark')), - high_contrast: Object.values(themes).filter((theme) => theme.labels.includes('high_contrast')), - low_contrast: Object.values(themes).filter((theme) => theme.labels.includes('low_contrast')), - monochrome: Object.values(themes).filter((theme) => theme.labels.includes('monochrome')), - greyscale: Object.values(themes).filter((theme) => theme.labels.includes('greyscale')), - protanopia_safe: Object.values(themes).filter((theme) => theme.labels.includes('protanopia_safe')), - deuteranopia_safe: Object.values(themes).filter((theme) => theme.labels.includes('deuteranopia_safe')), - tritanopia_safe: Object.values(themes).filter((theme) => theme.labels.includes('tritanopia_safe')), - }, - extras: await load_extras(), - made_directories: new Set(), - build_time: { - iso: now.toISO(), - rfc2822: now.toRFC2822(), - }, - } - - if (conf.input.raw) { - await copy_raw_files(state); - } - - if (conf.input.text) { - await render_text_file_templates(state); - } - - if (conf.input.markdown) { - await render_markdown_files(state); - } - - if (conf.input['schema+json'] || conf.input['schema+yaml']) { - await render_json_schema_files(state); - } - - // todo... -} - - - -// ===== Raw File Copy ===== - -async function copy_raw_files(state: BuildState) { - const promises: Promise[] = [ ]; - const files = await glob(state.conf.input.raw, { - absolute: true, - cwd: state.conf.input.root, - }); - - for (const in_file of files) { - if (state.seen_files.has(in_file)) { - continue; - } - - state.seen_files.add(in_file); - - const out_file = map_input_file_to_output_file(state, in_file); - - promises.push( - fs.copyFile(in_file, await out_file) - ); - } - - await Promise.all(promises); -} - - - -// ===== File Renderers ===== - -async function render_text_file_templates(state: BuildState) { - const promises: Promise[] = [ ]; - const files = await glob(state.conf.input.text, { - absolute: true, - cwd: state.conf.input.root, - }); - - if (! state.partials) { - await build_partials(state); - } - - for (const in_file of files) { - if (state.seen_files.has(in_file)) { - continue; - } - - state.seen_files.add(in_file); - - promises.push( - render_text_file_template(state, in_file) - ); - } - - await Promise.all(promises); -} - -async function render_text_file_template(state: BuildState, in_file: string) { - const out_file = map_input_file_to_output_file(state, in_file); - const { frontmatter, text } = await read_text(in_file); - - let layout: string; - const layout_file = frontmatter?.layout; - - if (layout_file) { - if (! state.layouts[layout_file]) { - state.layouts[layout_file] = await load_layout(state.conf, layout_file); - } - - layout = state.layouts[layout_file]; - } - - const tags = state.conf.templates?.tags; - const context = mustache_context(state, frontmatter); - const rendered = render_template(text, context, layout, structuredClone(state.partials), tags); - await write_text(await out_file, rendered); -} - -async function render_markdown_files(state: BuildState) { - const promises: Promise[] = [ ]; - const files = await glob(state.conf.input.markdown, { - absolute: true, - cwd: state.conf.input.root, - }); - - if (! state.partials) { - await build_partials(state); - } - - for (const in_file of files) { - if (state.seen_files.has(in_file)) { - continue; - } - - state.seen_files.add(in_file); - - promises.push( - render_markdown_file(state, in_file) - ); - } - - await Promise.all(promises); -} - -async function render_markdown_file(state: BuildState, in_file: string) { - const out_file = map_input_file_to_output_file(state, in_file, [ '.md', '.markdown' ], '.html'); - const { frontmatter, text } = await read_text(in_file); - - const html = render_markdown_to_html(text, state.conf.markdown); - - let layout: string; - const layout_file = frontmatter?.layout; - - if (layout_file) { - if (! state.layouts[layout_file]) { - state.layouts[layout_file] = await load_layout(state.conf, layout_file); - } - - layout = state.layouts[layout_file]; - } - - const tags = state.conf.templates?.tags; - const context = mustache_context(state, frontmatter); - const rendered = render_template(await html, context, layout, structuredClone(state.partials), tags); - await write_text(await out_file, rendered); -} - -async function render_json_schema_files(state: BuildState) { - const promises: Promise[] = [ ]; - const json_files = await glob(state.conf.input['schema+json'], { - absolute: true, - cwd: state.conf.input.root, - }); - const yaml_files = await glob(state.conf.input['schema+yaml'], { - absolute: true, - cwd: state.conf.input.root, - }); - - if (! state.partials) { - await build_partials(state); - } - - for (const in_file of json_files) { - if (state.seen_files.has(in_file)) { - continue; - } - - state.seen_files.add(in_file); - - promises.push( - handle_json_schema_json_file(state, in_file) - ); - } - - for (const in_file of yaml_files) { - if (state.seen_files.has(in_file)) { - continue; - } - - state.seen_files.add(in_file); - - promises.push( - handle_json_schema_yaml_file(state, in_file) - ); - } - - await Promise.all(promises); -} - -async function handle_json_schema_json_file(state: BuildState, in_file: string) { - const promises: Promise[] = [ ]; - - const out_file = map_input_file_to_output_file(state, in_file, [ '.json' ], '.html'); - const { frontmatter, parsed, json } = await read_json(in_file, true); - - promises.push( - render_json_schema(state, parsed, await out_file, frontmatter) - ); - - if (state.conf.output.include_inputs?.includes('schema+json')) { - const json_file = await map_input_file_to_output_file(state, in_file); - - promises.push( - write_text(json_file, json) - ); - - if (state.conf.output.include_yaml_and_json) { - const yaml_file = await map_input_file_to_output_file(state, in_file, [ '.json' ], '.yaml'); - - promises.push( - write_text(yaml_file, to_yaml(parsed)) - ); - } - } - - await Promise.all(promises); -} - -async function handle_json_schema_yaml_file(state: BuildState, in_file: string) { - const promises: Promise[] = [ ]; - - const out_file = map_input_file_to_output_file(state, in_file, [ '.yaml', '.yml' ], '.html'); - const { frontmatter, parsed, yaml } = await read_yaml(in_file, true); - - promises.push( - render_json_schema(state, parsed, await out_file, frontmatter) - ); - - if (state.conf.output.include_inputs?.includes('schema+yaml')) { - const yaml_file = await map_input_file_to_output_file(state, in_file); - - promises.push( - write_text(yaml_file, yaml) - ); - - if (state.conf.output.include_yaml_and_json) { - const json_file = await map_input_file_to_output_file(state, in_file, [ '.yaml', '.yml' ], '.json'); - - promises.push( - write_text(json_file, JSON.stringify(parsed, null, ' ')) - ); - } - } - - await Promise.all(promises); -} - -async function render_json_schema(state: BuildState, schema: unknown, out_file: string, frontmatter?: any) { - const promises: Promise[] = [ ]; - const markdown = build_markdown_from_json_schema(schema); - - if (state.conf.output.include_intermediate_markdown) { - promises.push( - map_input_file_to_output_file(state, out_file, [ '.html' ], '.md') - .then((md_file) => write_text(md_file, markdown)) - ); - } - - const html = render_markdown_to_html(markdown, state.conf.markdown); - - let layout: string; - const layout_file = frontmatter?.layout; - - if (layout_file) { - if (! state.layouts[layout_file]) { - state.layouts[layout_file] = await load_layout(state.conf, layout_file); - } - - layout = state.layouts[layout_file]; - } - - const tags = state.conf.templates?.tags; - const context = mustache_context(state, frontmatter); - const rendered = render_template(await html, context, layout, structuredClone(state.partials), tags); - - promises.push( - write_text(await out_file, rendered) - ); - - await Promise.all(promises); -} - - - - - -// ===== Helpers ===== - -async function map_input_file_to_output_file(state: BuildState, in_file: string, remove_exts?: string[], add_ext?: string) { - if (! in_file.startsWith(state.conf.input.root)) { - throw new Error('input file expected to be inside input root'); - } - - let out_file = path_join(state.conf.output.root, in_file.slice(state.conf.input.root.length)); - - if (remove_exts) { - for (const ext of remove_exts) { - if (out_file.endsWith(ext)) { - out_file = out_file.slice(0, -ext.length); - break; - } - } - } - - if (add_ext) { - out_file += add_ext; - } - - const dir = dirname(out_file); - - if (! state.made_directories.has(dir)) { - state.made_directories.add(dir); - await mkdirp(dir); - } - - return out_file; -} - -async function build_partials(state: BuildState) { - state.partials = await load_partials(state.conf); - - for (const [name, theme] of Object.entries(state.themes)) { - state.partials[`.themes/${name}`] = render_theme_css_properties(theme); - } - - Object.assign(state.partials, state.extras); -} - -function mustache_context(state: BuildState, frontmatter?: FrontMatter) : Context { - return { - env: state.env, - page: frontmatter, - build_time: state.build_time, - icons: icons, - themes: Object.values(state.themes), - theme_groups: structuredClone(state.theme_groups), - markdown: { - render_inline() { - return (text, render) => { - const md = render(text) - const html = render_markdown_to_html_inline_sync(md, state.conf.markdown); - return html; - }; - }, - } - }; -} diff --git a/src/conf.ts b/src/conf.ts index 47058bc..e36590c 100644 --- a/src/conf.ts +++ b/src/conf.ts @@ -14,7 +14,21 @@ export async function read_config(file: string) { return config; } +export interface RSSConfig { + dir_path?: string; + out_file?: string; + front_matter_field?: string; +} + +export interface CalendarConfig { + dir_path?: string; + out_file?: string; + front_matter_field?: string; +} + export interface Config { + metadata: string; + base_url: string; input: { root: string; raw?: string[]; @@ -39,7 +53,16 @@ export interface Config { include_yaml_and_json?: boolean; include_intermediate_markdown?: boolean; }; - markdown?: MarkdownOptions; + sitemap?: false | { + out_file?: string; + front_matter_field?: string; + }; + rss?: false | RSSConfig[]; + events?: false | { + front_matter_field?: string; + }; + calendars?: false | CalendarConfig[]; + markdown?: Omit; schema?: { // }; @@ -57,6 +80,7 @@ function resolve_paths(file_path: string, config: Config) { const base_path = dirname(file_path); config.input.root = resolve_path(base_path, config.input.root); config.output.root = resolve_path(base_path, config.output.root); + config.metadata = resolve_path(base_path, config.metadata); if (config.templates?.layouts) { config.templates.layouts = resolve_path(base_path, config.templates.layouts); @@ -65,6 +89,36 @@ function resolve_paths(file_path: string, config: Config) { if (config.templates?.partials) { config.templates.partials = resolve_path(base_path, config.templates.partials); } + + if ((config.sitemap as boolean) === true) { + config.sitemap = { }; + } + + if ((config.events as boolean) === true) { + config.events = { }; + } + + if (Array.isArray(config.rss)) { + for (const rss_conf of config.rss) { + if (rss_conf.dir_path) { + rss_conf.dir_path = resolve_path(config.input.root, rss_conf.dir_path); + rss_conf.out_file = resolve_path(config.output.root, rss_conf.out_file); + } + } + } + + if (config.sitemap && typeof config.sitemap === 'object' && config.sitemap.out_file) { + config.sitemap.out_file = resolve_path(config.output.root, config.sitemap.out_file); + } + + if (Array.isArray(config.calendars)) { + for (const cal_conf of config.calendars) { + if (cal_conf.dir_path) { + cal_conf.dir_path = resolve_path(config.input.root, cal_conf.dir_path); + cal_conf.out_file = resolve_path(config.output.root, cal_conf.out_file); + } + } + } } function process_markdown_config(config: any) { diff --git a/src/fs.ts b/src/fs.ts index 6f45fd3..b6b2f11 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -3,6 +3,7 @@ import { process_frontmatter } from '@doc-utils/markdown2html'; import { promises as fs } from 'fs'; import { resolve as resolve_path } from 'path'; import { parse as parse_yaml, stringify as to_yaml } from 'yaml'; +import { hash_str } from './hash'; export async function load_from_dir(dir: string, file: string) { if (! dir) { @@ -18,41 +19,58 @@ export function mkdirp(dir: string, mode = 0o700) { return fs.mkdir(dir, { mode, recursive: true }); } -export async function read_text(file: string, check_for_frontmatter = true) { +export async function with_default_if_missing(read_promise: Promise, default_value: T) : Promise { + try { + return await read_promise; + } + + catch (error) { + if (error?.code === 'ENOENT') { + return default_value; + } + + throw error; + } +} + +export async function read_text(file: string, check_for_frontmatter = true, compute_hash = true) { const text = await fs.readFile(file, 'utf8'); + const hash = compute_hash ? hash_str(text) : null; if (check_for_frontmatter) { const { frontmatter, document } = process_frontmatter(text); - return { frontmatter, text: document }; + return { frontmatter, text: document, hash }; } - return { text, frontmatter: null }; + return { text, frontmatter: null, hash }; } -export async function read_json(file: string, check_for_frontmatter = true) { +export async function read_json(file: string, check_for_frontmatter = true, compute_hash = true) { const json = await fs.readFile(file, 'utf8'); + const hash = compute_hash ? hash_str(json) : null; if (check_for_frontmatter) { const { frontmatter, document } = process_frontmatter(json); const parsed = JSON.parse(document); - return { frontmatter, json: document, parsed }; + return { frontmatter, json: document, parsed, hash }; } const parsed = JSON.parse(json); - return { json, parsed, frontmatter: null }; + return { json, parsed, frontmatter: null, hash }; } -export async function read_yaml(file: string, check_for_frontmatter = true) { +export async function read_yaml(file: string, check_for_frontmatter = true, compute_hash = true) { const yaml = await fs.readFile(file, 'utf8'); + const hash = compute_hash ? hash_str(yaml) : null; if (check_for_frontmatter) { const { frontmatter, document } = process_frontmatter(yaml); const parsed = parse_yaml(document); - return { frontmatter, yaml: document, parsed }; + return { frontmatter, yaml: document, parsed, hash }; } const parsed = parse_yaml(yaml); - return { yaml, parsed, frontmatter: null }; + return { yaml, parsed, frontmatter: null, hash }; } export function write_text(file: string, text: string) { diff --git a/src/hash.ts b/src/hash.ts new file mode 100644 index 0000000..72b108f --- /dev/null +++ b/src/hash.ts @@ -0,0 +1,14 @@ + +import { createHash } from 'crypto'; + +export function hash_str(str: string) { + const hash = createHash('sha512'); + hash.update(str); + return hash.digest('base64'); +} + +export function hash_obj(obj: any) { + // todo: use something more stable + const str = JSON.stringify(obj); + return hash_str(str); +} diff --git a/src/index.ts b/src/index.ts index 27693c1..d86be65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { read_config, Config } from './conf'; -export { build_docs_project, ThemeGroups } from './build'; +export { build_docs_project, ThemeGroups } from './build-files'; diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 0000000..0dd326c --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,16 @@ + +export interface Metadata { + last_build: { + time: string; + config_hash: string; + }; + files: { + [file_path: string]: { + first_seen_time: string; + last_build_hash: string; + last_updated_time: string; + }; + }; +} + +// diff --git a/src/template.ts b/src/template.ts index 3371e8a..6c9987f 100644 --- a/src/template.ts +++ b/src/template.ts @@ -6,11 +6,13 @@ import { resolve as resolve_path } from 'path'; import { glob } from 'glob'; import { load_from_dir } from './fs'; import { ColorTheme } from '@doc-utils/color-themes'; -import { ThemeGroups } from './build'; +import { ThemeGroups } from './build-files'; export interface Context { env?: Record; page?: FrontMatter; + base_url: string; + page_url: string; icons: Record; themes: ColorTheme[]; theme_groups: ThemeGroups; @@ -43,11 +45,11 @@ export async function load_extras() { 'components/outline-inline.js', 'prism.css', 'typography/spacious.css', - 'typography/dense.css', + 'typography/compact.css', 'typography/general.css', 'theme-animation.css', 'forms-inputs/spacious.css', - 'forms-inputs/dense.css', + 'forms-inputs/compact.css', 'forms-inputs/general.css', ];