diff --git a/.gitignore b/.gitignore
index 3618675..dd87e2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,2 @@
node_modules
build
-docs
-www
diff --git a/extras/components/color-scheme-toggle-button.js b/extras/components/color-scheme-toggle-button.js
new file mode 100644
index 0000000..dbb6d65
--- /dev/null
+++ b/extras/components/color-scheme-toggle-button.js
@@ -0,0 +1,175 @@
+(() => {
+
+ const animation = 'linear .5s';
+ const color_scheme = 'color_scheme';
+ const color_scheme_attr = 'data-color-scheme';
+ const transition_attr = 'data-color-transition-enabled';
+ const prefers_dark_scheme = window.matchMedia('(prefers-color-scheme: dark)');
+
+ // Make sure we set the correct starting color scheme where loading
+
+ const override = localStorage.getItem(color_scheme);
+
+ if (override) {
+ document.body.setAttribute(color_scheme_attr, override);
+ }
+
+ setTimeout(() => {
+ if (document.body.scrollHeight > 30000) {
+ console.warn('document too large, disabling color theme transition animation');
+ return;
+ }
+
+ document.body.setAttribute(transition_attr, '');
+ }, 50);
+
+ const size = 1.25;
+
+ const styles = `
+ :host {
+ display: contents;
+ }
+
+ :host .wrapper {
+ width: ${size}rem;
+ height: ${size}rem;
+ padding: 0.5rem;
+ border-radius: 100%;
+ cursor: pointer;
+ overflow: hidden;
+ position: relative;
+ }
+
+ :host .wrapper .icons {
+ width: ${size * 2}rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ transition: transform ${animation};
+ pointer-events: none;
+ }
+
+ :host .wrapper .icons[data-mode='light'] {
+ /* */
+ }
+
+ :host .wrapper .icons[data-mode='dark'] {
+ transform: translateX(-${size}rem);
+ }
+
+ :host svg.icon {
+ --icon-size: ${size}rem;
+ transition: opacity ${animation};
+ }
+
+ :host svg.icon.sun {
+ color: var(--theme-text-body);
+ }
+
+ :host [data-mode='dark'] svg.icon.sun {
+ opacity: 0;
+ }
+
+ :host svg.icon.moon {
+ color: var(--theme-text-body);
+ }
+
+ :host [data-mode='light'] svg.icon.moon {
+ opacity: 0;
+ }
+ `;
+
+ const template = `
+
+
+
+ {{{ icons.sun }}}
+ {{{ icons.moon }}}
+
+
+ `;
+
+ customElements.define('color-scheme-toggle-button',
+ class ColorSchemeToggleButton extends HTMLElement {
+ #wrapper = null;
+ #icons = null;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.shadowRoot.innerHTML = template;
+ this.#wrapper = this.shadowRoot.querySelector('.wrapper');
+ this.#icons = this.shadowRoot.querySelector('.icons');
+ this.update_icons();
+ }
+
+ connectedCallback() {
+ this.addEventListener('click', this.onSelect);
+ this.addEventListener('keydown', this.onKeydown);
+ this.addEventListener('keyup', this.onKeyup);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener('click', this.onSelect);
+ this.removeEventListener('keydown', this.onKeydown);
+ this.removeEventListener('keyup', this.onKeyup);
+ }
+
+ onKeydown = (/** @type KeyboardEvent */ event) => {
+ if (event.keyCode === 32) {
+ event.preventDefault();
+ }
+
+
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ this.onSelect();
+ }
+ };
+
+ onKeyup = (/** @type KeyboardEvent */ event) => {
+ if (event.keyCode === 32) {
+ event.preventDefault();
+ this.onSelect();
+ }
+ };
+
+ onSelect = () => {
+ toggle();
+ this.update_icons();
+ };
+
+ update_icons() {
+ const current = get_current();
+
+ this.#icons.setAttribute('data-mode', current);
+ this.#wrapper.setAttribute('aria-pressed', current === 'dark');
+ }
+ }
+ );
+
+ function get_current() {
+ const override = localStorage.getItem(color_scheme);
+
+ if (override) {
+ return override;
+ }
+
+ return prefers_dark_scheme.matches ? 'dark' : 'light';
+ }
+
+ function toggle() {
+ if (document.body.hasAttribute(color_scheme_attr)) {
+ localStorage.removeItem(color_scheme);
+ document.body.removeAttribute(color_scheme_attr);
+ }
+
+ else {
+ const preference = prefers_dark_scheme.matches ? 'light' : 'dark';
+
+ localStorage.setItem(color_scheme, preference);
+ document.body.setAttribute(color_scheme_attr, preference);
+ }
+ }
+
+})();
\ No newline at end of file
diff --git a/extras/components/outline-button.js b/extras/components/outline-button.js
new file mode 100644
index 0000000..07bc2df
--- /dev/null
+++ b/extras/components/outline-button.js
@@ -0,0 +1,129 @@
+(() => {
+
+ const size = 1.25;
+ const outline_attr = 'data-show-outline';
+
+ const styles = `
+ :host {
+ display: contents;
+ }
+
+ :host .wrapper {
+ width: ${size}rem;
+ height: ${size}rem;
+ padding: 0.5rem;
+ border-radius: 100%;
+ cursor: pointer;
+ overflow: hidden;
+ position: relative;
+ }
+
+ :host svg.icon {
+ --icon-size: ${size}rem;
+ color: var(--theme-text-body);
+ position: absolute;
+ top: 0.5rem;
+ left: 0.5rem;
+ pointer-events: none;
+ transition: color linear .5s;
+ }
+ `;
+
+ const template = `
+
+
+ {{{ icons.list }}}
+
+
+ `;
+
+ customElements.define('outline-button',
+ class OutlineButton extends HTMLElement {
+ #wrapper = null;
+ #outline = null;
+ #outline_built = false;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.shadowRoot.innerHTML = template;
+ this.#wrapper = this.shadowRoot.querySelector('.wrapper');
+
+ this.#outline = this.shadowRoot.querySelector('#outline-panel');
+ this.#outline.parentNode.removeChild(this.#outline);
+ document.body.appendChild(this.#outline);
+ }
+
+ connectedCallback() {
+ this.addEventListener('click', this.onSelect);
+ this.addEventListener('keydown', this.onKeydown);
+ this.addEventListener('keyup', this.onKeyup);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener('click', this.onSelect);
+ this.removeEventListener('keydown', this.onKeydown);
+ this.removeEventListener('keyup', this.onKeyup);
+ }
+
+ onKeydown = (/** @type KeyboardEvent */ event) => {
+ if (event.keyCode === 32) {
+ event.preventDefault();
+ }
+
+
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ this.onSelect();
+ }
+ };
+
+ onKeyup = (/** @type KeyboardEvent */ event) => {
+ if (event.keyCode === 32) {
+ event.preventDefault();
+ this.onSelect();
+ }
+ };
+
+ onSelect = () => {
+ if (! this.#outline_built) {
+ const root_selector = this.getAttribute('content-root');
+ this.#outline.innerHTML += `${build_outline(root_selector)}
`;
+ this.#outline_built = true;
+ }
+
+ if (document.body.hasAttribute(outline_attr)) {
+ document.body.removeAttribute(outline_attr);
+ this.#outline.setAttribute('aria-hidden', 'true');
+ this.#wrapper.setAttribute('aria-pressed', 'false');
+ }
+
+ else {
+ document.body.setAttribute(outline_attr, '');
+ this.#outline.removeAttribute('aria-hidden');
+ this.#wrapper.setAttribute('aria-pressed', 'true');
+ }
+ };
+ }
+ );
+
+ function build_outline(root_selector) {
+ const root = document.querySelector(root_selector);
+ const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
+ const outline = [ ];
+
+ for (const heading of headings) {
+ outline.push(`
+
+ ${heading.innerText}
+
+ `);
+ }
+
+ return outline.join('');
+ }
+
+})();
\ No newline at end of file
diff --git a/extras/components/outline-inline.js b/extras/components/outline-inline.js
new file mode 100644
index 0000000..910fba6
--- /dev/null
+++ b/extras/components/outline-inline.js
@@ -0,0 +1,105 @@
+(() => {
+
+ const template = `
+
+
+ `;
+
+ customElements.define('outline-inline',
+ class OutlineButton extends HTMLElement {
+ #outline = null;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.shadowRoot.innerHTML = template;
+ this.#outline = this.shadowRoot.querySelector('#outline-inline');
+ }
+
+ connectedCallback() {
+ if (document.readyState === 'complete') {
+ this.#render();
+ }
+
+ window.addEventListener('DOMContentLoaded', () => {
+ this.#render();
+ });
+ }
+
+ #render() {
+ const root_selector = this.getAttribute('content-root');
+ const outline = build_outline(root_selector);
+ this.#outline.innerHTML = `${outline}
`;
+ }
+ }
+ );
+
+ function build_outline(root_selector) {
+ const root = document.querySelector(root_selector);
+ const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
+ const outline = [ ];
+
+ for (const heading of headings) {
+ outline.push(`
+
+ ${heading.innerText}
+
+ `);
+ }
+
+ return outline.join('');
+ }
+
+})();
\ No newline at end of file
diff --git a/extras/prism.css b/extras/prism.css
new file mode 100644
index 0000000..c68bb4a
--- /dev/null
+++ b/extras/prism.css
@@ -0,0 +1,468 @@
+/* PrismJS 1.24.1
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+http+jsx+tsx+typescript&plugins=line-highlight+line-numbers+treeview */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ *
+ * ---
+ * Modified for use with customizable color variables and light/dark theming
+ */
+
+ code[class*="language-"],
+ pre[class*="language-"] {
+ color: var(--theme-code-normal);
+ background: none;
+ text-shadow: 0 1px var(--theme-code-shadow);
+ font-family: var(--font-monospace);
+ font-weight: 400;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ word-wrap: normal;
+ line-height: 1.5;
+
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+ }
+
+ pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+ code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
+ text-shadow: none;
+ background: var(--theme-code-selection);
+ }
+
+ pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+ code[class*="language-"]::selection, code[class*="language-"] ::selection {
+ text-shadow: none;
+ background: var(--theme-code-selection);
+ }
+
+ @media print {
+ code[class*="language-"],
+ pre[class*="language-"] {
+ text-shadow: none;
+ }
+ }
+
+ /* Code blocks */
+ pre[class*="language-"] {
+ overflow: auto;
+ }
+
+ :not(pre) > code[class*="language-"],
+ pre[class*="language-"] {
+ background: var(--theme-code-background);
+ }
+
+ /* Inline code */
+ :not(pre) > code[class*="language-"] {
+ padding: .1rem;
+ border-radius: .3rem;
+ white-space: normal;
+ }
+
+ .token.comment,
+ .token.prolog,
+ .token.doctype,
+ .token.cdata {
+ color: var(--theme-code-comment);
+ }
+
+ .token.punctuation {
+ color: var(--theme-code-punc);
+ }
+
+ .token.namespace {
+ opacity: .7;
+ }
+
+ .token.constant,
+ .token.symbol,
+ .token.deleted {
+ color: var(--theme-code-const-literal);
+ }
+
+ .token.tag {
+ color: var(--theme-code-tag);
+ }
+
+ .token.number {
+ color: var(--theme-code-number-literal);
+ }
+
+ .token.boolean {
+ color: var(--theme-code-boolean-literal);
+ }
+
+ .token.selector,
+ .token.attr-name {
+ color: var(--theme-code-attr-name);
+ }
+
+ .token.builtin {
+ color: var(--theme-code-builtin);
+ }
+
+ .token.string,
+ .token.char,
+ .token.inserted {
+ color: var(--theme-code-string);
+ }
+
+ .token.operator,
+ .token.entity,
+ .token.url,
+ .language-css .token.string,
+ .style .token.string {
+ color: var(--theme-code-operator);
+ }
+
+ .token.atrule,
+ .token.attr-value,
+ .token.keyword,
+ .token.request-line .token.method,
+ .token.request-line .token.http-version,
+ .token.response-status .token.http-version {
+ color: var(--theme-code-keyword);
+ }
+
+ .token.function {
+ color: var(--theme-code-func-name);
+ }
+
+ .token.class-name {
+ color: var(--theme-code-class-name);
+ }
+
+ .token.regex,
+ .token.important {
+ color: var(--theme-code-regex-important);
+ }
+
+ .token.property,
+ .token.variable {
+ color: var(--theme-code-variable);
+ }
+
+ .token.important,
+ .token.bold {
+ font-weight: 700;
+ }
+
+ .token.italic {
+ font-style: italic;
+ }
+
+ .token.entity {
+ cursor: help;
+ }
+
+ pre[data-line] {
+ position: relative;
+ padding: 1rem 0 1rem 3rem;
+ }
+
+ .line-highlight {
+ position: absolute;
+ left: 0;
+ right: 0;
+ padding: inherit 0;
+ margin-top: 1rem;
+
+ background: var(--theme-code-line-highlight);
+
+ pointer-events: none;
+
+ line-height: inherit;
+ white-space: pre;
+ }
+
+ @media print {
+ .line-highlight {
+ /*
+ * This will prevent browsers from replacing the background color with white.
+ * It's necessary because the element is layered on top of the displayed code.
+ */
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+ }
+ }
+
+ .line-highlight:before,
+ .line-highlight[data-end]:after {
+ content: attr(data-start);
+ position: absolute;
+ top: .4rem;
+ left: .6rem;
+ min-width: 1rem;
+ padding: 0 .5rem;
+ background: var(--theme-code-line-highlight);
+ font: bold 65%/1.5 sans-serif;
+ text-align: center;
+ vertical-align: .3rem;
+ border-radius: 999px;
+ text-shadow: none;
+ box-shadow: 0 1px white;
+ }
+
+ .line-highlight[data-end]:after {
+ content: attr(data-end);
+ top: auto;
+ bottom: .4rem;
+ }
+
+ .line-numbers .line-highlight:before,
+ .line-numbers .line-highlight:after {
+ content: none;
+ }
+
+ pre[id].linkable-line-numbers span.line-numbers-rows {
+ pointer-events: all;
+ }
+
+ pre[id].linkable-line-numbers span.line-numbers-rows > span:before {
+ cursor: pointer;
+ }
+
+ pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before {
+ /* TODO: Do something with this color */
+ background-color: rgba(128, 128, 128, .2);
+ }
+
+ pre[class*="language-"].line-numbers {
+ position: relative;
+ padding-left: 3.8rem;
+ counter-reset: linenumber;
+ }
+
+ pre[class*="language-"].line-numbers > code {
+ position: relative;
+ white-space: inherit;
+ }
+
+ .line-numbers .line-numbers-rows {
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ font-size: 100%;
+ left: -3.8rem;
+ width: 3rem; /* works for line-numbers below 1000 lines */
+ letter-spacing: -1px;
+ border-right: 1px solid var(--theme-code-gutter-divider);
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ }
+
+ .line-numbers-rows > span {
+ display: block;
+ counter-increment: linenumber;
+ }
+
+ .line-numbers-rows > span:before {
+ content: counter(linenumber);
+ color: var(--theme-code-line-number);
+ display: block;
+ padding-right: 0.8rem;
+ text-align: right;
+ }
+
+ .token.treeview-part .entry-line {
+ position: relative;
+ text-indent: -99rem;
+ display: inline-block;
+ vertical-align: top;
+ width: 1.2rem;
+ }
+
+ .token.treeview-part .entry-line:before,
+ .token.treeview-part .line-h:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: 50%;
+ height: 100%;
+ }
+
+ /* TODO: Do something with these colors */
+
+ .token.treeview-part .line-h:before,
+ .token.treeview-part .line-v:before {
+ border-left: 1px solid #ccc;
+ }
+
+ .token.treeview-part .line-v-last:before {
+ height: 50%;
+ border-left: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ }
+
+ .token.treeview-part .line-h:after {
+ height: 50%;
+ border-bottom: 1px solid #ccc;
+ }
+
+ .token.treeview-part .entry-name {
+ position: relative;
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .token.treeview-part .entry-name.dotfile {
+ opacity: 0.5;
+ }
+
+ /* @GENERATED-FONT */
+ @font-face {
+ font-family: "PrismTreeview";
+ /**
+ * This font is generated from the .svg files in the `icons` folder. See the `treeviewIconFont` function in
+ * `gulpfile.js/index.js` for more information.
+ *
+ * Use the following escape sequences to refer to a specific icon:
+ *
+ * - \ea01 file
+ * - \ea02 folder
+ * - \ea03 image
+ * - \ea04 audio
+ * - \ea05 video
+ * - \ea06 text
+ * - \ea07 code
+ * - \ea08 archive
+ * - \ea09 pdf
+ * - \ea0a excel
+ * - \ea0b powerpoint
+ * - \ea0c word
+ */
+ src: url("data:application/font-woff;base64,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")
+ format("woff");
+ }
+
+ .token.treeview-part .entry-name:before {
+ content: "\ea01";
+ font-family: "PrismTreeview";
+ font-size: inherit;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ width: 2.5ex;
+ display: inline-block;
+ }
+
+ .token.treeview-part .entry-name.dir:before {
+ content: "\ea02";
+ }
+
+ .token.treeview-part .entry-name.ext-bmp:before,
+ .token.treeview-part .entry-name.ext-eps:before,
+ .token.treeview-part .entry-name.ext-gif:before,
+ .token.treeview-part .entry-name.ext-jpe:before,
+ .token.treeview-part .entry-name.ext-jpg:before,
+ .token.treeview-part .entry-name.ext-jpeg:before,
+ .token.treeview-part .entry-name.ext-png:before,
+ .token.treeview-part .entry-name.ext-svg:before,
+ .token.treeview-part .entry-name.ext-tiff:before {
+ content: "\ea03";
+ }
+
+ .token.treeview-part .entry-name.ext-cfg:before,
+ .token.treeview-part .entry-name.ext-conf:before,
+ .token.treeview-part .entry-name.ext-config:before,
+ .token.treeview-part .entry-name.ext-csv:before,
+ .token.treeview-part .entry-name.ext-ini:before,
+ .token.treeview-part .entry-name.ext-log:before,
+ .token.treeview-part .entry-name.ext-md:before,
+ .token.treeview-part .entry-name.ext-nfo:before,
+ .token.treeview-part .entry-name.ext-txt:before {
+ content: "\ea06";
+ }
+
+ .token.treeview-part .entry-name.ext-asp:before,
+ .token.treeview-part .entry-name.ext-aspx:before,
+ .token.treeview-part .entry-name.ext-c:before,
+ .token.treeview-part .entry-name.ext-cc:before,
+ .token.treeview-part .entry-name.ext-cpp:before,
+ .token.treeview-part .entry-name.ext-cs:before,
+ .token.treeview-part .entry-name.ext-css:before,
+ .token.treeview-part .entry-name.ext-h:before,
+ .token.treeview-part .entry-name.ext-hh:before,
+ .token.treeview-part .entry-name.ext-htm:before,
+ .token.treeview-part .entry-name.ext-html:before,
+ .token.treeview-part .entry-name.ext-jav:before,
+ .token.treeview-part .entry-name.ext-java:before,
+ .token.treeview-part .entry-name.ext-js:before,
+ .token.treeview-part .entry-name.ext-php:before,
+ .token.treeview-part .entry-name.ext-rb:before,
+ .token.treeview-part .entry-name.ext-xml:before {
+ content: "\ea07";
+ }
+
+ .token.treeview-part .entry-name.ext-7z:before,
+ .token.treeview-part .entry-name.ext-bz:before,
+ .token.treeview-part .entry-name.ext-bz2:before,
+ .token.treeview-part .entry-name.ext-gz:before,
+ .token.treeview-part .entry-name.ext-rar:before,
+ .token.treeview-part .entry-name.ext-tar:before,
+ .token.treeview-part .entry-name.ext-tgz:before,
+ .token.treeview-part .entry-name.ext-zip:before {
+ content: "\ea08";
+ }
+
+ .token.treeview-part .entry-name.ext-aac:before,
+ .token.treeview-part .entry-name.ext-au:before,
+ .token.treeview-part .entry-name.ext-cda:before,
+ .token.treeview-part .entry-name.ext-flac:before,
+ .token.treeview-part .entry-name.ext-mp3:before,
+ .token.treeview-part .entry-name.ext-oga:before,
+ .token.treeview-part .entry-name.ext-ogg:before,
+ .token.treeview-part .entry-name.ext-wav:before,
+ .token.treeview-part .entry-name.ext-wma:before {
+ content: "\ea04";
+ }
+
+ .token.treeview-part .entry-name.ext-avi:before,
+ .token.treeview-part .entry-name.ext-flv:before,
+ .token.treeview-part .entry-name.ext-mkv:before,
+ .token.treeview-part .entry-name.ext-mov:before,
+ .token.treeview-part .entry-name.ext-mp4:before,
+ .token.treeview-part .entry-name.ext-mpeg:before,
+ .token.treeview-part .entry-name.ext-mpg:before,
+ .token.treeview-part .entry-name.ext-ogv:before,
+ .token.treeview-part .entry-name.ext-webm:before {
+ content: "\ea05";
+ }
+
+ .token.treeview-part .entry-name.ext-pdf:before {
+ content: "\ea09";
+ }
+
+ .token.treeview-part .entry-name.ext-xls:before,
+ .token.treeview-part .entry-name.ext-xlsx:before {
+ content: "\ea0a";
+ }
+
+ .token.treeview-part .entry-name.ext-doc:before,
+ .token.treeview-part .entry-name.ext-docm:before,
+ .token.treeview-part .entry-name.ext-docx:before {
+ content: "\ea0c";
+ }
+
+ .token.treeview-part .entry-name.ext-pps:before,
+ .token.treeview-part .entry-name.ext-ppt:before,
+ .token.treeview-part .entry-name.ext-pptx:before {
+ content: "\ea0b";
+ }
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 5b7ab8c..313d592 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "@doc-utils/docs2website",
"version": "0.1.0",
"dependencies": {
+ "@doc-utils/color-themes": "^0.1.1",
+ "@doc-utils/jsonschema2markdown": "^0.1.0",
"@doc-utils/markdown2html": "^0.1.0",
"glob": "^10.2.2",
"luxon": "^3.3.0",
@@ -26,10 +28,28 @@
"typescript": "^5.0.4"
}
},
+ "node_modules/@doc-utils/color-themes": {
+ "version": "0.1.14",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.14/color-themes-0.1.14.tgz",
+ "integrity": "sha512-j0U8v8Y+9zAm9D7pbCheTQYGEKt9FSpKSZQNGsogxWl95S9Z7QMjtmJns6QPgdOsSDss7sjMLFS5Gm50GCMzNA=="
+ },
+ "node_modules/@doc-utils/jsonschema2markdown": {
+ "version": "0.1.1",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fjsonschema2markdown/-/0.1.1/jsonschema2markdown-0.1.1.tgz",
+ "integrity": "sha512-d3H5Jk7QXesu++3C/vaxnzXP6QkNHdOmMmbkSMywkInC5BC0RpUqtav5KheDZ7PzrnNO37YjdMODvdaMNUFYCw==",
+ "dependencies": {
+ "glob": "^10.2.2",
+ "json-schema": "^0.4.0",
+ "luxon": "^3.3.0",
+ "mustache": "^4.2.0",
+ "word-wrap": "^1.2.3",
+ "yaml": "^2.2.2"
+ }
+ },
"node_modules/@doc-utils/markdown2html": {
- "version": "0.1.6",
- "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.6/markdown2html-0.1.6.tgz",
- "integrity": "sha512-lfb0vW0effsyKOXCXXF8d+xWjsKCF2rUuOq5F0GdOsw4PSJP3yCBW9oWZs4JdajwigGCBhWvXviyZeKInSdzgg==",
+ "version": "0.1.20",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.20/markdown2html-0.1.20.tgz",
+ "integrity": "sha512-kAkGITASEdT4jKPyeAAKZ/9QBmU1sOm0as3nHn9IolkZDydn02pmMIJCi4mrRWbj/3uj/WY5VtRa6T7sqYczNg==",
"dependencies": {
"bytefield-svg": "^1.6.1",
"dompurify": "^2.3.6",
@@ -111,9 +131,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "18.16.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz",
- "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==",
+ "version": "18.16.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz",
+ "integrity": "sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==",
"dev": true
},
"node_modules/@types/prismjs": {
@@ -919,9 +939,9 @@
}
},
"node_modules/glob": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz",
- "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==",
+ "version": "10.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz",
+ "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.0.3",
@@ -1086,6 +1106,11 @@
}
}
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
+ },
"node_modules/katex": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.7.tgz",
@@ -1211,9 +1236,9 @@
}
},
"node_modules/node-fetch": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
- "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
+ "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -1341,11 +1366,11 @@
}
},
"node_modules/path-scurry": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz",
- "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz",
+ "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==",
"dependencies": {
- "lru-cache": "^9.0.0",
+ "lru-cache": "^9.1.1",
"minipass": "^5.0.0"
},
"engines": {
@@ -1500,9 +1525,9 @@
}
},
"node_modules/signal-exit": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz",
- "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
+ "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==",
"engines": {
"node": ">=14"
},
@@ -2425,10 +2450,28 @@
}
},
"dependencies": {
+ "@doc-utils/color-themes": {
+ "version": "0.1.14",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.14/color-themes-0.1.14.tgz",
+ "integrity": "sha512-j0U8v8Y+9zAm9D7pbCheTQYGEKt9FSpKSZQNGsogxWl95S9Z7QMjtmJns6QPgdOsSDss7sjMLFS5Gm50GCMzNA=="
+ },
+ "@doc-utils/jsonschema2markdown": {
+ "version": "0.1.1",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fjsonschema2markdown/-/0.1.1/jsonschema2markdown-0.1.1.tgz",
+ "integrity": "sha512-d3H5Jk7QXesu++3C/vaxnzXP6QkNHdOmMmbkSMywkInC5BC0RpUqtav5KheDZ7PzrnNO37YjdMODvdaMNUFYCw==",
+ "requires": {
+ "glob": "^10.2.2",
+ "json-schema": "^0.4.0",
+ "luxon": "^3.3.0",
+ "mustache": "^4.2.0",
+ "word-wrap": "^1.2.3",
+ "yaml": "^2.2.2"
+ }
+ },
"@doc-utils/markdown2html": {
- "version": "0.1.6",
- "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.6/markdown2html-0.1.6.tgz",
- "integrity": "sha512-lfb0vW0effsyKOXCXXF8d+xWjsKCF2rUuOq5F0GdOsw4PSJP3yCBW9oWZs4JdajwigGCBhWvXviyZeKInSdzgg==",
+ "version": "0.1.20",
+ "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.1.20/markdown2html-0.1.20.tgz",
+ "integrity": "sha512-kAkGITASEdT4jKPyeAAKZ/9QBmU1sOm0as3nHn9IolkZDydn02pmMIJCi4mrRWbj/3uj/WY5VtRa6T7sqYczNg==",
"requires": {
"bytefield-svg": "^1.6.1",
"dompurify": "^2.3.6",
@@ -2501,9 +2544,9 @@
"dev": true
},
"@types/node": {
- "version": "18.16.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz",
- "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==",
+ "version": "18.16.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz",
+ "integrity": "sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==",
"dev": true
},
"@types/prismjs": {
@@ -3091,9 +3134,9 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"glob": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz",
- "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==",
+ "version": "10.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz",
+ "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==",
"requires": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.0.3",
@@ -3209,6 +3252,11 @@
"xml-name-validator": "^4.0.0"
}
},
+ "json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
+ },
"katex": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.7.tgz",
@@ -3291,9 +3339,9 @@
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
"node-fetch": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
- "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
+ "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
"requires": {
"whatwg-url": "^5.0.0"
},
@@ -3385,11 +3433,11 @@
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
"path-scurry": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz",
- "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz",
+ "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==",
"requires": {
- "lru-cache": "^9.0.0",
+ "lru-cache": "^9.1.1",
"minipass": "^5.0.0"
}
},
@@ -3504,9 +3552,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"signal-exit": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz",
- "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw=="
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
+ "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q=="
},
"source-map": {
"version": "0.6.1",
diff --git a/package.json b/package.json
index f0c8598..381572f 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,12 @@
},
"scripts": {
"tsc": "tsc",
- "clean": "rm -rf ./build"
+ "clean": "rm -rf ./build ./www"
},
"bin": {
"docs2website": "./bin/docs2website"
},
- "exports": "./build/index.js",
+ "main": "./build/index.js",
"devDependencies": {
"@types/jsdom": "^20.0.0",
"@types/luxon": "^3.1.0",
@@ -21,6 +21,8 @@
"typescript": "^5.0.4"
},
"dependencies": {
+ "@doc-utils/color-themes": "^0.1.1",
+ "@doc-utils/jsonschema2markdown": "^0.1.0",
"@doc-utils/markdown2html": "^0.1.0",
"glob": "^10.2.2",
"luxon": "^3.3.0",
diff --git a/sample-config.yaml b/sample-config.yaml
deleted file mode 100644
index 9749508..0000000
--- a/sample-config.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-# Input Configuration (where your documents are)
-input:
- # The root directory for where to find input file. All other
- # input paths are relative to this, and must be under this
- root: ./docs
-
- # Files identified in this section are copied over to the
- # output directory unprocessed
- raw:
- - ./**/*.{png,jpg,jpeg,gif}
-
- # Files in this section will be processed as mustache templates,
- # but will receive no other processing
- text:
- - ./**/*.{css,html,js,txt}
-
- # Files to be parsed as markdown (optionally with front matter)
- # and rendered to HTML pages
- markdown:
- - ./**/*.md
-
- # Files to be parsed as JSON Schema definitions and rendered
- # to HTML documentation. Additionally, the original JSON / Yaml
- # file will also be copied to the output directory, unaltered
- schema+json:
- - ./**/*.schema.json
- schema+yaml:
- - ./**/*.schema.{yaml,yml}
-
- # Files to be parsed as OpenAPI V3 specifications and rendered
- # to HTML documentation. Additionally, the original JSON / Yaml
- # file will also be copied to the output directory, unaltered
- openapi+json:
- - ./**/*.openapi.json
- openapi+yaml:
- - ./**/*.openapi.{yaml,yml}
-
-# Template Configuration (used by mustache to actually render pages)
-templates:
- # Root directory where layout files are stored
- layouts: ./layouts
-
- # Root directory where partial files are stored
- partials: ./partials
-
- # (Optional) whitelist of environment variables to be made accessible
- # under `env` when processing templates
- env:
- - EXAMPLE_ENVIRONMENT_VARIABLE
- - FOO_BAR_BAZ
-
-# Output Configuration (where to put your website)
-output:
- # The root directory to output your website at. The path of an
- # input file relative to $.input.root will match (aside from file
- # extension) the path of the output file relative to $.output.root
- root: ./www
-
-# Markdown-to-HTML Configuration
-markdown:
- #
diff --git a/src/bin/docs2website.ts b/src/bin/docs2website.ts
index 9e09a41..57a3e00 100644
--- a/src/bin/docs2website.ts
+++ b/src/bin/docs2website.ts
@@ -5,6 +5,6 @@ import { build_docs_project } from '../build';
main();
async function main() {
- const conf = await read_config('./sample-config.yaml');
+ const conf = await read_config(process.argv[2]);
await build_docs_project(conf);
}
diff --git a/src/build.ts b/src/build.ts
index 77c4f9b..b818043 100644
--- a/src/build.ts
+++ b/src/build.ts
@@ -1,13 +1,20 @@
+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 { Config } from './conf';
import { promises as fs } from 'fs';
import { dirname, join as path_join } from 'path';
import { build_env_scope } from './env';
-import { process_frontmatter } from '@doc-utils/markdown2html';
-import { load_layout, Context, render_template, load_partials } from './template';
+import { load_layout, Context, render_template, load_partials, load_extras, FrontMatter } from './template';
import { DateTime } from 'luxon';
import assert = require('assert');
+import { load_themes, render_theme_css_properties } from './themes';
+import { icons } from './icons';
+import { mkdirp, read_json, read_text, read_yaml, write_text } from './fs';
+import { stringify as to_yaml } from 'yaml';
interface BuildState {
conf: Config;
@@ -15,6 +22,9 @@ interface BuildState {
env: Record;
partials?: Record;
layouts: Record;
+ themes: Record;
+ theme_groups: ThemeGroups;
+ extras: Record;
made_directories: Set;
build_time: {
iso: string;
@@ -22,13 +32,42 @@ interface BuildState {
};
}
+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(),
@@ -45,12 +84,20 @@ export async function build_docs_project(conf: Config) {
}
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, {
@@ -68,13 +115,17 @@ async function copy_raw_files(state: BuildState) {
const out_file = map_input_file_to_output_file(state, in_file);
promises.push(
- fs.copyFile(in_file, await out_file, 0o600)
+ 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, {
@@ -83,7 +134,7 @@ async function render_text_file_templates(state: BuildState) {
});
if (! state.partials) {
- state.partials = await load_partials(state.conf);
+ await build_partials(state);
}
for (const in_file of files) {
@@ -103,7 +154,7 @@ async function render_text_file_templates(state: BuildState) {
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, document } = process_frontmatter(await fs.readFile(in_file, 'utf8'));
+ const { frontmatter, text } = await read_text(in_file);
let layout: string;
const layout_file = frontmatter?.layout;
@@ -116,16 +167,202 @@ async function render_text_file_template(state: BuildState, in_file: string) {
layout = state.layouts[layout_file];
}
- const context: Context = {
- env: state.env,
- page: frontmatter,
- build_time: state.build_time,
- };
-
- const rendered = render_template(document, context, layout, structuredClone(state.partials));
- await fs.writeFile(await out_file, rendered, 'utf8');
+ 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) {
assert(in_file.startsWith(state.conf.input.root), '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));
@@ -147,11 +384,38 @@ async function map_input_file_to_output_file(state: BuildState, in_file: string,
if (! state.made_directories.has(dir)) {
state.made_directories.add(dir);
- await fs.mkdir(dir, {
- mode: 0o700,
- recursive: true,
- });
+ 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 48aaabe..47058bc 100644
--- a/src/conf.ts
+++ b/src/conf.ts
@@ -2,12 +2,14 @@
import { promises as fs } from 'fs';
import { parse as parse_yaml } from 'yaml';
import { resolve as resolve_path, dirname } from 'path';
+import { MarkdownOptions } from '@doc-utils/markdown2html';
export async function read_config(file: string) {
const path = resolve_path(process.cwd(), file);
const yaml = await fs.readFile(path, 'utf8');
const config = parse_yaml(yaml);
validate_config(config);
+ process_markdown_config(config);
resolve_paths(path, config);
return config;
}
@@ -27,14 +29,17 @@ export interface Config {
templates?: {
layouts?: string;
partials?: string;
+ themes?: string;
+ tags?: [ string, string ];
env?: string[];
};
output: {
root: string;
+ include_inputs?: string[];
+ include_yaml_and_json?: boolean;
+ include_intermediate_markdown?: boolean;
};
- markdown?: {
- //
- };
+ markdown?: MarkdownOptions;
schema?: {
//
};
@@ -61,3 +66,19 @@ function resolve_paths(file_path: string, config: Config) {
config.templates.partials = resolve_path(base_path, config.templates.partials);
}
}
+
+function process_markdown_config(config: any) {
+ if (config?.markdown?.custom_elements) {
+ if (config.markdown.custom_elements.tag_names) {
+ const tags = new Set(config.markdown.custom_elements.tag_names);
+ config.markdown.custom_elements.tagNameCheck = (tag_name) => tags.has(tag_name);
+ delete config.markdown.custom_elements.tag_names;
+ }
+
+ if (config.markdown.custom_elements.attribute_names) {
+ const attrs = new Set(config.markdown.custom_elements.attribute_names);
+ config.markdown.custom_elements.attributeNameCheck = (attr_name) => attrs.has(attr_name);
+ delete config.markdown.custom_elements.attribute_names;
+ }
+ }
+}
diff --git a/src/fs.ts b/src/fs.ts
new file mode 100644
index 0000000..6f45fd3
--- /dev/null
+++ b/src/fs.ts
@@ -0,0 +1,70 @@
+
+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';
+
+export async function load_from_dir(dir: string, file: string) {
+ if (! dir) {
+ return null;
+ }
+
+ const rel_path = resolve_path('/', file);
+ const abs_path = resolve_path(dir, '.' + rel_path);
+ return await fs.readFile(abs_path, 'utf8');
+}
+
+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) {
+ const text = await fs.readFile(file, 'utf8');
+
+ if (check_for_frontmatter) {
+ const { frontmatter, document } = process_frontmatter(text);
+ return { frontmatter, text: document };
+ }
+
+ return { text, frontmatter: null };
+}
+
+export async function read_json(file: string, check_for_frontmatter = true) {
+ const json = await fs.readFile(file, 'utf8');
+
+ if (check_for_frontmatter) {
+ const { frontmatter, document } = process_frontmatter(json);
+ const parsed = JSON.parse(document);
+ return { frontmatter, json: document, parsed };
+ }
+
+ const parsed = JSON.parse(json);
+ return { json, parsed, frontmatter: null };
+}
+
+export async function read_yaml(file: string, check_for_frontmatter = true) {
+ const yaml = await fs.readFile(file, 'utf8');
+
+ if (check_for_frontmatter) {
+ const { frontmatter, document } = process_frontmatter(yaml);
+ const parsed = parse_yaml(document);
+ return { frontmatter, yaml: document, parsed };
+ }
+
+ const parsed = parse_yaml(yaml);
+ return { yaml, parsed, frontmatter: null };
+}
+
+export function write_text(file: string, text: string) {
+ return fs.writeFile(file, text, 'utf8');
+}
+
+export function write_json(file: string, obj: any, pretty = true) {
+ const json = JSON.stringify(obj, null, pretty ? ' ' : null);
+ return fs.writeFile(file, json, 'utf8');
+}
+
+export function write_yaml(file: string, obj: any) {
+ const yaml = to_yaml(obj);
+ return fs.writeFile(file, yaml, 'utf8');
+}
diff --git a/src/icons.ts b/src/icons.ts
new file mode 100644
index 0000000..01a4775
--- /dev/null
+++ b/src/icons.ts
@@ -0,0 +1,23 @@
+
+export const icons: Record = Object.create(null);
+
+const whitespace = /[\s\t\n]+/g;
+const feather_icons: Record = require('../vendor/feather-icons/icons.json');
+
+for (const [name, contents] of Object.entries(feather_icons)) {
+ icons[name] = `
+
+ `.replace(whitespace, ' ').trim();
+}
+
+Object.freeze(icons);
diff --git a/src/index.ts b/src/index.ts
index 74af906..27693c1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,3 @@
export { read_config, Config } from './conf';
+export { build_docs_project, ThemeGroups } from './build';
diff --git a/src/template.ts b/src/template.ts
index 0a1250d..f20dce8 100644
--- a/src/template.ts
+++ b/src/template.ts
@@ -4,35 +4,57 @@ import { render as mustache_render } from 'mustache';
import { promises as fs } from 'fs';
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';
export interface Context {
env?: Record;
- page?: {
- title?: string;
- layout?: string;
- [key: string]: string | number | boolean;
- };
+ page?: FrontMatter;
+ icons: Record;
+ themes: ColorTheme[];
+ theme_groups: ThemeGroups;
build_time: {
iso: string;
rfc2822: string;
};
+ markdown: {
+ render_inline(): MustacheRenderer;
+ }
}
-export function render_template(template: string, context: Context, layout?: string, partials?: Record) {
+export interface FrontMatter {
+ title?: string;
+ layout?: string;
+ [key: string]: unknown;
+}
+
+export function render_template(template: string, context: Context, layout?: string, partials: Record = { }, tags?: [ string, string ]) {
partials['.content'] = template;
- return mustache_render(layout || template, context, partials);
+ return mustache_render(layout || template, context, partials, tags);
+}
+
+export async function load_extras() {
+ const extras: Record = Object.create(null);
+ const extras_dir = resolve_path(__dirname, '../extras');
+ const extras_files = [
+ 'components/color-scheme-toggle-button.js',
+ 'components/outline-button.js',
+ 'components/outline-inline.js',
+ 'prism.css',
+ ];
+
+ const promises = extras_files.map((file) => load_from_dir(extras_dir, file));
+
+ for (let i = 0; i < extras_files.length; i++) {
+ extras[`.extras/${extras_files[i]}`] = await promises[i];
+ }
+
+ return extras;
}
export async function load_layout(conf: Config, file: string) {
- const path = conf.templates?.layouts;
-
- if (! path) {
- return null;
- }
-
- const rel_path = resolve_path('/', file);
- const abs_path = resolve_path(path, '.' + rel_path);
- return await fs.readFile(abs_path, 'utf8');
+ return load_from_dir(conf.templates?.layouts, file);
}
export async function load_partials(conf: Config) {
@@ -55,3 +77,7 @@ export async function load_partials(conf: Config) {
return partials;
}
+
+export interface MustacheRenderer {
+ (this: void, text: string, render: (text: string) => string): string;
+}
diff --git a/src/themes.ts b/src/themes.ts
new file mode 100644
index 0000000..4b01089
--- /dev/null
+++ b/src/themes.ts
@@ -0,0 +1,64 @@
+
+import { glob } from 'glob';
+import { Config } from './conf';
+import { load_from_dir } from './fs';
+import { ColorTheme, themes as builtin_themes, load_theme as load_builtin_theme, validate_theme } from '@doc-utils/color-themes';
+
+export async function load_themes(conf: Config) {
+ const themes: Record = { };
+
+ for (const theme of builtin_themes) {
+ themes[theme] = load_builtin_theme(theme);
+ }
+
+ if (conf.templates?.themes) {
+ const local_themes = await glob(conf.templates.themes + '/**/theme.json');
+
+ for (const theme_path of local_themes) {
+ const segments = theme_path.split('/');
+ const theme = segments[segments.length - 2];
+ themes[theme] = await load_local_theme(conf, theme);
+ }
+ }
+
+ return themes;
+}
+
+export async function load_theme(conf: Config, name: string) {
+ if (name.includes('/')) {
+ return null;
+ }
+
+ const local_theme = await load_local_theme(conf, name);
+
+ if (local_theme) {
+ return local_theme;
+ }
+
+ return load_builtin_theme(name);
+}
+
+export async function load_local_theme(conf: Config, name: string) {
+ let json: string;
+
+ try {
+ json = await load_from_dir(conf.templates?.themes, `./${name}/theme.json`);
+ }
+
+ catch (error) {
+ return null;
+ }
+
+ const theme = JSON.parse(json);
+ validate_theme(theme);
+ return theme;
+}
+
+export function render_theme_css_properties(theme: ColorTheme) {
+ return Object
+ .entries(theme.colors)
+ .map(([ name, color ]) => {
+ return `--theme-${name.replace(/_/g, '-')}: ${color};`;
+ })
+ .join('\n') + '\n';
+}
diff --git a/vendor/feather-icons/icons.json b/vendor/feather-icons/icons.json
new file mode 100644
index 0000000..ba2001c
--- /dev/null
+++ b/vendor/feather-icons/icons.json
@@ -0,0 +1,289 @@
+{
+ "activity": "",
+ "airplay": "",
+ "alert-circle": "",
+ "alert-octagon": "",
+ "alert-triangle": "",
+ "align-center": "",
+ "align-justify": "",
+ "align-left": "",
+ "align-right": "",
+ "anchor": "",
+ "aperture": "",
+ "archive": "",
+ "arrow-down-circle": "",
+ "arrow-down-left": "",
+ "arrow-down-right": "",
+ "arrow-down": "",
+ "arrow-left-circle": "",
+ "arrow-left": "",
+ "arrow-right-circle": "",
+ "arrow-right": "",
+ "arrow-up-circle": "",
+ "arrow-up-left": "",
+ "arrow-up-right": "",
+ "arrow-up": "",
+ "at-sign": "",
+ "award": "",
+ "bar-chart-2": "",
+ "bar-chart": "",
+ "battery-charging": "",
+ "battery": "",
+ "bell-off": "",
+ "bell": "",
+ "bluetooth": "",
+ "bold": "",
+ "book-open": "",
+ "book": "",
+ "bookmark": "",
+ "box": "",
+ "briefcase": "",
+ "calendar": "",
+ "camera-off": "",
+ "camera": "",
+ "cast": "",
+ "check-circle": "",
+ "check-square": "",
+ "check": "",
+ "chevron-down": "",
+ "chevron-left": "",
+ "chevron-right": "",
+ "chevron-up": "",
+ "chevrons-down": "",
+ "chevrons-left": "",
+ "chevrons-right": "",
+ "chevrons-up": "",
+ "chrome": "",
+ "circle": "",
+ "clipboard": "",
+ "clock": "",
+ "cloud-drizzle": "",
+ "cloud-lightning": "",
+ "cloud-off": "",
+ "cloud-rain": "",
+ "cloud-snow": "",
+ "cloud": "",
+ "code": "",
+ "codepen": "",
+ "codesandbox": "",
+ "coffee": "",
+ "columns": "",
+ "command": "",
+ "compass": "",
+ "copy": "",
+ "corner-down-left": "",
+ "corner-down-right": "",
+ "corner-left-down": "",
+ "corner-left-up": "",
+ "corner-right-down": "",
+ "corner-right-up": "",
+ "corner-up-left": "",
+ "corner-up-right": "",
+ "cpu": "",
+ "credit-card": "",
+ "crop": "",
+ "crosshair": "",
+ "database": "",
+ "delete": "",
+ "disc": "",
+ "divide-circle": "",
+ "divide-square": "",
+ "divide": "",
+ "dollar-sign": "",
+ "download-cloud": "",
+ "download": "",
+ "dribbble": "",
+ "droplet": "",
+ "edit-2": "",
+ "edit-3": "",
+ "edit": "",
+ "external-link": "",
+ "eye-off": "",
+ "eye": "",
+ "facebook": "",
+ "fast-forward": "",
+ "feather": "",
+ "figma": "",
+ "file-minus": "",
+ "file-plus": "",
+ "file-text": "",
+ "file": "",
+ "film": "",
+ "filter": "",
+ "flag": "",
+ "folder-minus": "",
+ "folder-plus": "",
+ "folder": "",
+ "framer": "",
+ "frown": "",
+ "gift": "",
+ "git-branch": "",
+ "git-commit": "",
+ "git-merge": "",
+ "git-pull-request": "",
+ "github": "",
+ "gitlab": "",
+ "globe": "",
+ "grid": "",
+ "hard-drive": "",
+ "hash": "",
+ "headphones": "",
+ "heart": "",
+ "help-circle": "",
+ "hexagon": "",
+ "home": "",
+ "image": "",
+ "inbox": "",
+ "info": "",
+ "instagram": "",
+ "italic": "",
+ "key": "",
+ "layers": "",
+ "layout": "",
+ "life-buoy": "",
+ "link-2": "",
+ "link": "",
+ "linkedin": "",
+ "list": "",
+ "loader": "",
+ "lock": "",
+ "log-in": "",
+ "log-out": "",
+ "mail": "",
+ "map-pin": "",
+ "map": "",
+ "maximize-2": "",
+ "maximize": "",
+ "meh": "",
+ "menu": "",
+ "message-circle": "",
+ "message-square": "",
+ "mic-off": "",
+ "mic": "",
+ "minimize-2": "",
+ "minimize": "",
+ "minus-circle": "",
+ "minus-square": "",
+ "minus": "",
+ "monitor": "",
+ "moon": "",
+ "more-horizontal": "",
+ "more-vertical": "",
+ "mouse-pointer": "",
+ "move": "",
+ "music": "",
+ "navigation-2": "",
+ "navigation": "",
+ "octagon": "",
+ "package": "",
+ "paperclip": "",
+ "pause-circle": "",
+ "pause": "",
+ "pen-tool": "",
+ "percent": "",
+ "phone-call": "",
+ "phone-forwarded": "",
+ "phone-incoming": "",
+ "phone-missed": "",
+ "phone-off": "",
+ "phone-outgoing": "",
+ "phone": "",
+ "pie-chart": "",
+ "play-circle": "",
+ "play": "",
+ "plus-circle": "",
+ "plus-square": "",
+ "plus": "",
+ "pocket": "",
+ "power": "",
+ "printer": "",
+ "radio": "",
+ "refresh-ccw": "",
+ "refresh-cw": "",
+ "repeat": "",
+ "rewind": "",
+ "rotate-ccw": "",
+ "rotate-cw": "",
+ "rss": "",
+ "save": "",
+ "scissors": "",
+ "search": "",
+ "send": "",
+ "server": "",
+ "settings": "",
+ "share-2": "",
+ "share": "",
+ "shield-off": "",
+ "shield": "",
+ "shopping-bag": "",
+ "shopping-cart": "",
+ "shuffle": "",
+ "sidebar": "",
+ "skip-back": "",
+ "skip-forward": "",
+ "slack": "",
+ "slash": "",
+ "sliders": "",
+ "smartphone": "",
+ "smile": "",
+ "speaker": "",
+ "square": "",
+ "star": "",
+ "stop-circle": "",
+ "sun": "",
+ "sunrise": "",
+ "sunset": "",
+ "table": "",
+ "tablet": "",
+ "tag": "",
+ "target": "",
+ "terminal": "",
+ "thermometer": "",
+ "thumbs-down": "",
+ "thumbs-up": "",
+ "toggle-left": "",
+ "toggle-right": "",
+ "tool": "",
+ "trash-2": "",
+ "trash": "",
+ "trello": "",
+ "trending-down": "",
+ "trending-up": "",
+ "triangle": "",
+ "truck": "",
+ "tv": "",
+ "twitch": "",
+ "twitter": "",
+ "type": "",
+ "umbrella": "",
+ "underline": "",
+ "unlock": "",
+ "upload-cloud": "",
+ "upload": "",
+ "user-check": "",
+ "user-minus": "",
+ "user-plus": "",
+ "user-x": "",
+ "user": "",
+ "users": "",
+ "video-off": "",
+ "video": "",
+ "voicemail": "",
+ "volume-1": "",
+ "volume-2": "",
+ "volume-x": "",
+ "volume": "",
+ "watch": "",
+ "wifi-off": "",
+ "wifi": "",
+ "wind": "",
+ "x-circle": "",
+ "x-octagon": "",
+ "x-square": "",
+ "x": "",
+ "youtube": "",
+ "zap-off": "",
+ "zap": "",
+ "zoom-in": "",
+ "zoom-out": ""
+}
\ No newline at end of file
diff --git a/vendor/feather-icons/license b/vendor/feather-icons/license
new file mode 100644
index 0000000..4bb4ff7
--- /dev/null
+++ b/vendor/feather-icons/license
@@ -0,0 +1,24 @@
+https://github.com/feathericons/feather/blob/master/LICENSE
+---
+
+The MIT License (MIT)
+
+Copyright (c) 2013-2017 Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/vendor/feather-icons/readme.md b/vendor/feather-icons/readme.md
new file mode 100644
index 0000000..84e0d9c
--- /dev/null
+++ b/vendor/feather-icons/readme.md
@@ -0,0 +1,6 @@
+
+https://github.com/feathericons/feather
+
+`icons.json` here is sourced from `dist/icons.json` from the bundle, version 4.29.0.
+
+This is intentionally not installed from `npm install feather-icons` because that package includes all of `core-js` as a dependency (which this project gets zero benefit from and is very large, impacting container image size).