From 5c6ef03082a6f47705ac56b6ae8741cf6993a0bd Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 27 May 2025 16:48:49 -0400 Subject: [PATCH 01/13] Update default stylesheet --- src/defaults/default-style.css | 52 +++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index bcd1dfd..faa00ef 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -1,16 +1,16 @@ :root { - --color-bg: #1d1d1d; - --color-text: #eee; - --color-link: #fff; - --color-strong: #ff8f8f; - --color-emphasis: #ff8f00; - --color-heading: #accfff; + --color-bg: hsl(0, 0%, 11%); + --color-text: hsl(0, 0%, 93%); + --color-link: hsl(0, 0%, 100%); + --color-strong: hsl(0, 100%, 78%); + --color-emphasis: hsl(34, 100%, 50%); + --color-heading: hsl(215, 100%, 84%); --font-size: 18px; - --font-family: Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", - source-sans-pro, sans-serif; + --font-family: system-ui, sans-serif; --font-weight: 400; --font-weight-bold: 500; + --line-height: 1.4; --max-width: 700px; @@ -39,7 +39,8 @@ h6 { font-family: var(--font-family); font-weight: var(--font-weight-bold); color: var(--color-heading); - margin: 1rem 0; + margin-top: 2rem; + margin-bottom: 0; } h1 { @@ -47,23 +48,33 @@ h1 { } h2 { - font-size: 2.75rem; + font-size: 2.5rem; + text-align: center; } h3 { - font-size: 2.15rem; -} - -h4 { - font-size: 1.75rem; -} - -h5 { font-size: 1.5rem; } +h4 { + font-size: 1.2rem; +} + +h5 { + font-size: 1.1rem; +} + h6 { - font-size: 1.25rem; + font-size: 1rem; +} + +h1 + p, +h2 + p, +h3 + p, +h4 + p, +h5 + p, +h6 + p { + margin-top: 0.5rem; } em, @@ -84,6 +95,7 @@ a { table { border-collapse: collapse; width: 100%; + max-width: 100%; margin: 1em 0; font-size: 1em; background-color: var(--color-bg); @@ -93,7 +105,7 @@ table { th, td { - padding: 0.75em 1em; + padding: 0.5em 0.5em; text-align: left; border-bottom: 1px solid var(--color-text); } From 6f4b2d20dadc55dc953a41316e573fd1b80ec44e Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 27 May 2025 17:00:12 -0400 Subject: [PATCH 02/13] Use unified, add header ids --- bun.lock | 205 +++++++++++++++++++++++++++++++++ demo/another.md | 4 + demo/index.md | 32 +++++ package.json | 10 +- src/defaults/default-style.css | 1 - src/index.ts | 5 +- src/markdown.ts | 20 ++++ 7 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 src/markdown.ts diff --git a/bun.lock b/bun.lock index 323e3d7..6f5a710 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,13 @@ "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", "marked": "^15.0.12", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "unified": "^11.0.5", }, "devDependencies": { "@types/bun": "latest", @@ -39,16 +46,214 @@ "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], } } diff --git a/demo/another.md b/demo/another.md index 7fdb6ca..a0dad4d 100644 --- a/demo/another.md +++ b/demo/another.md @@ -1,3 +1,7 @@ +--- +title: Another File | Demo +--- + Here's another file. It has a list! diff --git a/demo/index.md b/demo/index.md index 9af8485..339807f 100644 --- a/demo/index.md +++ b/demo/index.md @@ -1,3 +1,35 @@ +--- +title: Home | Demo +--- + # Demo Data This is just a folder of markdown files for testing purposes. + +## Here's a level 2 heading + +And some content underneath that. How about a few lorem ipsums? + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +## Another section + +Tables should be a thing too + +| One | Two | Three | +| - | - | - | +| A cell | Another cell | And another | +| Another row | another cell | and another | + +**Bold Stuff** as well as *emphasis* and such. + +- A bulleted list +- Is always helpful +- When you don't +- Need numbers + +HTML works, still! diff --git a/package.json b/package.json index 91e2583..c891984 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "0.0.1", + "version": "0.1.0", "devDependencies": { "@types/bun": "latest" }, @@ -13,6 +13,12 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", - "marked": "^15.0.12" + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "unified": "^11.0.5" } } diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index faa00ef..ef7d9b3 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -49,7 +49,6 @@ h1 { h2 { font-size: 2.5rem; - text-align: center; } h3 { diff --git a/src/index.ts b/src/index.ts index 76dc376..928b88c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import defaultTemplate from "./defaults/default-template.html" with { }; import { CLIError } from "./error"; import type { BunFile } from "bun"; +import { parseMarkdown } from "./markdown"; const DEFAULT_TEMPLATE_FILE = "_template.html"; const DEFAULT_STYLESHEET_FILE = "_style.css"; @@ -155,9 +156,7 @@ async function buildFile( content: string; }; - const html = await marked.parse(content, { - gfm: true, - }); + const html = await parseMarkdown(content); const title = [options.title, props.title].find((t) => t) || "Untitled"; diff --git a/src/markdown.ts b/src/markdown.ts new file mode 100644 index 0000000..1fdbe98 --- /dev/null +++ b/src/markdown.ts @@ -0,0 +1,20 @@ +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import rehypeStringify from "rehype-stringify"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import rehypeSlug from "rehype-slug"; +import { unified } from "unified"; + +export async function parseMarkdown(markdown: string): Promise { + const file = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeSlug) + .use(rehypeStringify) + .process(markdown); + + return String(file); +} From d39e7f51540a5ed4b7a651718a5555a4fe3aa6e3 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 27 May 2025 17:09:11 -0400 Subject: [PATCH 03/13] Remove unused import --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 928b88c..7dba246 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import Path from "node:path"; import { parseArgs } from "node:util"; import EMDY from "@endeavorance/emdy"; -import { marked } from "marked"; import defaultStylesheet from "./defaults/default-style.css" with { type: "text", }; From 515217e3470feb6703e07390d37f9ff975af0e9a Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 4 Jun 2025 19:07:01 -0400 Subject: [PATCH 04/13] Cleanup, commenting --- src/index.ts | 109 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7dba246..ab83f80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,26 @@ import Path from "node:path"; import { parseArgs } from "node:util"; import EMDY from "@endeavorance/emdy"; -import defaultStylesheet from "./defaults/default-style.css" with { +import type { BunFile } from "bun"; +import DEFAULT_STYLESHEET from "./defaults/default-style.css" with { type: "text", }; -import defaultTemplate from "./defaults/default-template.html" with { +import DEFAULT_TEMPLATE from "./defaults/default-template.html" with { type: "text", }; import { CLIError } from "./error"; -import type { BunFile } from "bun"; import { parseMarkdown } from "./markdown"; const DEFAULT_TEMPLATE_FILE = "_template.html"; const DEFAULT_STYLESHEET_FILE = "_style.css"; +/** + * Given a file path, attempt to read the text of the file + * @param filePath - The path to the file to read + * @returns The text content of the file + * + * @throws CLIError if the file does not exist + */ async function readFile(filePath: string): Promise { const file = Bun.file(filePath); const exists = await file.exists(); @@ -23,7 +30,13 @@ async function readFile(filePath: string): Promise { return file.text(); } -function replacePlaceholders( +/** + * Renders the provided template with the given data. + * @param template - The template string containing placeholders like %key% + * @param data - An object containing key-value pairs to replace in the template + * @returns The rendered string with all placeholders replaced by their corresponding values + */ +function renderTemplate( template: string, data: Record, ): string { @@ -34,17 +47,39 @@ function replacePlaceholders( return output; } +/** + * A collection of options parsed from the command line arguments. + */ interface CLIOptions { + /** A path to the output file */ outfile: string | null; + + /** A path to the output directory */ outdir: string | null; + + /** If true, force output to stdout */ stdout: boolean; + + /** The path to the template file */ templateFilePath: string | null; + + /** The path to the stylesheet file */ stylesheetFilePath: string | null; + + /** If true, show help message */ help: boolean; + + /** If provided, overrides the document title */ title: string | null; + + /** If provided, overrides the current working directory (default: .) */ cwd: string; } +/** + * Parse the command line arguments and return the options and positional arguments. + * @returns An object containing the parsed options and positional arguments + */ function parseCLIArgs(): { options: CLIOptions; args: string[] } { const { values: flags, positionals } = parseArgs({ args: Bun.argv.slice(2), @@ -109,57 +144,69 @@ function parseCLIArgs(): { options: CLIOptions; args: string[] } { }; } +/** + * Attempt to load a custom template, falling back to a default + * @param options - The CLI options containing the template file path + * @returns The template string + */ async function loadTemplate(options: CLIOptions): Promise { if (options.templateFilePath) { return readFile(options.templateFilePath); } - const defaultTemplateFilePath = Path.join(options.cwd, DEFAULT_TEMPLATE_FILE); - const defaultTemplateFile = Bun.file(defaultTemplateFilePath); + const checkTemplateFile = Bun.file( + Path.join(options.cwd, DEFAULT_TEMPLATE_FILE), + ); - if (await defaultTemplateFile.exists()) { - return defaultTemplateFile.text(); + if (await checkTemplateFile.exists()) { + return checkTemplateFile.text(); } - return defaultTemplate; + return DEFAULT_TEMPLATE; } +/** + * Attempt to load a custom stylesheet, falling back to a default + * @param options - The CLI options containing the stylesheet file path + * @returns The stylesheet string + */ async function loadStylesheet(options: CLIOptions): Promise { if (options.stylesheetFilePath) { return readFile(options.stylesheetFilePath); } - const defaultStylesheetFilePath = Path.join( - options.cwd, - DEFAULT_STYLESHEET_FILE, + const checkStylesheetFile = Bun.file( + Path.join(options.cwd, DEFAULT_STYLESHEET_FILE), ); - const defaultStylesheetFile = Bun.file(defaultStylesheetFilePath); - if (await defaultStylesheetFile.exists()) { - return defaultStylesheetFile.text(); + if (await checkStylesheetFile.exists()) { + return checkStylesheetFile.text(); } - return defaultStylesheet; + return DEFAULT_STYLESHEET; } +/** + * Build and write a file + * @param infile - The input file to read from + * @param outfile - The output file to write to + * @param options - The CLI options containing template and stylesheet paths + */ async function buildFile( infile: BunFile, outfile: BunFile, options: CLIOptions, -): Promise { +): Promise { const input = await infile.text(); const template = await loadTemplate(options); const stylesheet = await loadStylesheet(options); - const { content, ...props } = EMDY.parse(input) as Record & { content: string; }; const html = await parseMarkdown(content); - - const title = [options.title, props.title].find((t) => t) || "Untitled"; - - const replacers = { + const title = options.title ?? props.title ?? "Untitled"; + const templateData = { content: html, title, stylesheet: stylesheet, @@ -168,13 +215,15 @@ async function buildFile( ...props, }; - const output = replacePlaceholders(template, replacers); - - Bun.write(outfile, output); - - return output; + await Bun.write(outfile, renderTemplate(template, templateData)); } +/** + * Get the output path based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output directory + * @returns The resolved output path + */ async function getOutputPath(inputPath: string, options: CLIOptions) { const inputDirname = Path.dirname(inputPath); const inputBasename = Path.basename(inputPath); @@ -189,6 +238,12 @@ async function getOutputPath(inputPath: string, options: CLIOptions) { return Path.join(inputDirname, outputBasename); } +/** + * Get the output file based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output file and directory + * @returns The BunFile object representing the output file + */ async function getOutputFile( inputPath: string, options: CLIOptions, From b280b29df6306244abde03b941da2401830ef292 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 4 Jun 2025 19:10:47 -0400 Subject: [PATCH 05/13] Clean up readme --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 61a1d89..21aac5c 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,23 @@ Options: ## Templates -`buildmd` uses simple HTML files as templates, replacing instances of `%key%` with the value of `key` in the metadata of the document. +`buildmd` uses HTML files as templates. Instances of `%key%` are replaced with value of `key` from the built document. -By default, `buildmd` will look for a `_template.html` file in the current working directory, unless an alternate template is specified with the `--template` or `-t` option. +You can provide a template path with the `--template` or `-t` flag. If no template flag is provided, `buildmd` +will look for a `_template.html` file in the current directory. If no template file is found, a simple +default template will be used. -The following metadata is available by default: +## Stylesheets + +`buildmd` supports custom stylesheets. You can specify a stylesheet with the `--stylesheet` or `-s` flag. + +If no stylesheet is specified, `buildmd` will look for a `_style.css` file in the current directory. + +If no stylesheet is found, a default stylesheet will be used. + +## Data + +The following keys are always available in the template data: - `title`: The title of the document - `content`: The rendered markdown content @@ -38,14 +50,6 @@ The following metadata is available by default: - `datetime` A localized string representing when the build occurred Additionally, any data defined in YAML frontmatter in the markdown file -will be available for use in the template. Properties defined in frontmatter +will be available in the template. Properties defined in frontmatter take precedence over the default metadata. -If no template file is discovered, `buildmd` will inject a default template that includes the stylesheet, title, and content in well-structured HTML. - -## Styles - -`buildmd` will automatically look for a `_style.css` file in the current working directory, unless an alternate stylesheet is specified with the `--stylesheet` or `-s` option. The contents of the discovered stylesheet file will be available in the template as `%stylesheet%`. - -In the event that no stylesheet is found, `buildmd` will inject a default stylesheet. - From 5687f3b02b335c49eb1613794f378399a33bd9c8 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 6 Jun 2025 11:50:03 -0400 Subject: [PATCH 06/13] Add basic support for callouts --- bun.lock | 10 +- demo/another.md | 9 ++ package.json | 3 +- src/defaults/default-style.css | 245 +++++++++++++++++++++++++++++++++ src/markdown.ts | 2 + 5 files changed, 265 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 6f5a710..bb6a4c4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", - "marked": "^15.0.12", + "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -90,10 +90,14 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], @@ -116,8 +120,6 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], @@ -204,6 +206,8 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "rehype-callouts": ["rehype-callouts@2.1.0", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "hastscript": "^9.0.1", "unist-util-visit": "^5.0.0" } }, "sha512-Als/wlYpXg2Rs0p/yofrkSH6IQzn1xh6cvBC5BHy5JVT3lGlzUvVT8wOyVqCEgf8Eun6cQ3f47rLLoaiyLkP0w=="], + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="], diff --git a/demo/another.md b/demo/another.md index a0dad4d..2792cae 100644 --- a/demo/another.md +++ b/demo/another.md @@ -8,3 +8,12 @@ It has a list! - See? - Fancy! + +How about a callout? + +> [!info] This is an info callout +> Here's some more info +> etc + +> [!warning]- This is collapsible +> And how! diff --git a/package.json b/package.json index c891984..4083909 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "0.1.0", + "version": "0.2.0", "devDependencies": { "@types/bun": "latest" }, @@ -13,6 +13,7 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", + "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index ef7d9b3..c7e2f9c 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -113,3 +113,248 @@ th { background: var(--color-bg); font-weight: 500; } + +/* Callout styles, from https://raw.githubusercontent.com/lin-stephanie/rehype-callouts/refs/heads/main/src/themes/obsidian/index.css */ + +[data-callout="note"] { + --rc-color-light: var(--callout-note-color-light, rgb(8, 109, 221)); + --rc-color-dark: var(--callout-note-color-dark, rgb(2, 122, 255)); +} + +[data-callout="abstract"] { + --rc-color-light: var(--callout-abstract-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-abstract-color-dark, rgb(83, 223, 221)); +} + +[data-callout="summary"] { + --rc-color-light: var(--callout-summary-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-summary-color-dark, rgb(83, 223, 221)); +} + +[data-callout="tldr"] { + --rc-color-light: var(--callout-tldr-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-tldr-color-dark, rgb(83, 223, 221)); +} + +[data-callout="info"] { + --rc-color-light: var(--callout-info-color-light, rgb(8, 109, 221)); + --rc-color-dark: var(--callout-info-color-dark, rgb(2, 122, 255)); +} + +[data-callout="todo"] { + --rc-color-light: var(--callout-todo-color-light, rgb(8, 109, 221)); + --rc-color-dark: var(--callout-todo-color-dark, rgb(2, 122, 255)); +} + +[data-callout="tip"] { + --rc-color-light: var(--callout-tip-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-tip-color-dark, rgb(83, 223, 221)); +} + +[data-callout="hint"] { + --rc-color-light: var(--callout-hint-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-hint-color-dark, rgb(83, 223, 221)); +} + +[data-callout="important"] { + --rc-color-light: var(--callout-important-color-light, rgb(0, 191, 188)); + --rc-color-dark: var(--callout-important-color-dark, rgb(83, 223, 221)); +} + +[data-callout="success"] { + --rc-color-light: var(--callout-success-color-light, rgb(8, 185, 78)); + --rc-color-dark: var(--callout-success-color-dark, rgb(68, 207, 110)); +} + +[data-callout="check"] { + --rc-color-light: var(--callout-check-color-light, rgb(8, 185, 78)); + --rc-color-dark: var(--callout-check-color-dark, rgb(68, 207, 110)); +} + +[data-callout="done"] { + --rc-color-light: var(--callout-done-color-light, rgb(8, 185, 78)); + --rc-color-dark: var(--callout-done-color-dark, rgb(68, 207, 110)); +} + +[data-callout="question"] { + --rc-color-light: var(--callout-question-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-question-color-dark, rgb(233, 151, 63)); +} + +[data-callout="help"] { + --rc-color-light: var(--callout-help-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-help-color-dark, rgb(233, 151, 63)); +} + +[data-callout="faq"] { + --rc-color-light: var(--callout-faq-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-faq-color-dark, rgb(233, 151, 63)); +} + +[data-callout="warning"] { + --rc-color-light: var(--callout-warning-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-warning-color-dark, rgb(233, 151, 63)); +} + +[data-callout="attention"] { + --rc-color-light: var(--callout-attention-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-attention-color-dark, rgb(233, 151, 63)); +} + +[data-callout="caution"] { + --rc-color-light: var(--callout-caution-color-light, rgb(236, 117, 0)); + --rc-color-dark: var(--callout-caution-color-dark, rgb(233, 151, 63)); +} + +[data-callout="failure"] { + --rc-color-light: var(--callout-failure-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-failure-color-dark, rgb(251, 70, 76)); +} + +[data-callout="missing"] { + --rc-color-light: var(--callout-missing-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-missing-color-dark, rgb(251, 70, 76)); +} + +[data-callout="fail"] { + --rc-color-light: var(--callout-fail-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-fail-color-dark, rgb(251, 70, 76)); +} + +[data-callout="danger"] { + --rc-color-light: var(--callout-danger-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-danger-color-dark, rgb(251, 70, 76)); +} + +[data-callout="error"] { + --rc-color-light: var(--callout-error-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-error-color-dark, rgb(251, 70, 76)); +} + +[data-callout="bug"] { + --rc-color-light: var(--callout-bug-color-light, rgb(233, 49, 71)); + --rc-color-dark: var(--callout-bug-color-dark, rgb(251, 70, 76)); +} + +[data-callout="example"] { + --rc-color-light: var(--callout-example-color-light, rgb(120, 82, 238)); + --rc-color-dark: var(--callout-example-color-dark, rgb(168, 130, 255)); +} + +[data-callout="quote"] { + --rc-color-light: var(--callout-quote-color-light, rgb(158, 158, 158)); + --rc-color-dark: var(--callout-quote-color-dark, rgb(158, 158, 158)); +} + +[data-callout="cite"] { + --rc-color-light: var(--callout-cite-color-light, rgb(158, 158, 158)); + --rc-color-dark: var(--callout-cite-color-dark, rgb(158, 158, 158)); +} + +.callout { + --rc-color-default: #888; + + overflow: hidden; + + width: 100%; + padding: 12px 12px 12px 24px; + border-radius: 4px; + margin: 1em 0; + + line-height: 1.3; + + mix-blend-mode: darken; + background-color: rgb( + from var(--rc-color-light, var(--rc-color-default)) r g b / + 0.1 + ); +} + +.dark .callout { + mix-blend-mode: lighten; + background-color: rgb( + from var(--rc-color-dark, var(--rc-color-default)) r g b / + 0.1 + ); +} + +.callout-title { + display: flex; + align-items: flex-start; + gap: 4px; + + color: var(--rc-color-light, var(--rc-color-default)); + font-size: inherit; +} + +.dark .callout-title { + color: var(--rc-color-dark, var(--rc-color-default)); +} + +.callout-title::-webkit-details-marker { + display: none; +} + +.callout-title-icon { + display: flex; + flex: 0 0 auto; + align-items: center; +} + +.callout-title-text { + color: inherit; + font-weight: 600; +} + +.callout-content { + overflow-x: auto; + padding: 0; + background-color: transparent; +} + +.callout[data-collapsible="true"] .callout-title { + cursor: pointer; +} + +.callout[data-collapsible="true"] .callout-fold-icon { + display: flex; + align-items: center; + padding-inline-end: 8px; +} + +.callout[data-collapsible="true"] > .callout-title .callout-fold-icon svg { + transform: rotate(-90deg); + transition: transform 100ms ease-in-out; +} + +.callout[data-collapsible="true"][open] + > .callout-title + .callout-fold-icon + svg { + transform: none; +} + +.callout-title-icon::after, +.callout-fold-icon::after { + content: "\200B"; +} + +.callout-title-icon svg, +.callout-fold-icon svg { + width: 18px; + height: 18px; +} + +@media (prefers-color-scheme: dark) { + .callout { + mix-blend-mode: lighten; + background-color: rgb( + from var(--rc-color-dark, var(--rc-color-default)) r g b / + 0.1 + ); + } + + .callout-title { + color: var(--rc-color-dark, var(--rc-color-default)); + } +} diff --git a/src/markdown.ts b/src/markdown.ts index 1fdbe98..e03dd76 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -5,6 +5,7 @@ import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; import rehypeSlug from "rehype-slug"; import { unified } from "unified"; +import rehypeCallouts from "rehype-callouts"; export async function parseMarkdown(markdown: string): Promise { const file = await unified() @@ -13,6 +14,7 @@ export async function parseMarkdown(markdown: string): Promise { .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeRaw) .use(rehypeSlug) + .use(rehypeCallouts) .use(rehypeStringify) .process(markdown); From e68923e87fe2927b5bca95e97d1929252c935f24 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 6 Jun 2025 12:00:22 -0400 Subject: [PATCH 07/13] Fix default style to prevent callouts overflowing --- src/defaults/default-style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index c7e2f9c..4299046 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -256,7 +256,6 @@ th { overflow: hidden; - width: 100%; padding: 12px 12px 12px 24px; border-radius: 4px; margin: 1em 0; From 51b3633077f7f659a816a89db6e48a12f82e4b59 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 6 Jun 2025 12:00:37 -0400 Subject: [PATCH 08/13] 0.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4083909..05fcb90 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "0.2.0", + "version": "0.2.1", "devDependencies": { "@types/bun": "latest" }, From 5dfb004a5982c68f6ce62ddd8c9938990abdbb99 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Sat, 7 Jun 2025 10:32:06 -0400 Subject: [PATCH 09/13] Support overflowing tables in default styles --- bun.lock | 5 ++ demo/another.md | 4 ++ demo/index.md | 6 +++ package.json | 1 + src/defaults/default-style.css | 17 ++++--- src/index.ts | 86 +++++++++++++++++++++++++--------- 6 files changed, 90 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index bb6a4c4..a9dd4de 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", + "chokidar": "^4.0.3", "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", @@ -72,6 +73,8 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -206,6 +209,8 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "rehype-callouts": ["rehype-callouts@2.1.0", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "hastscript": "^9.0.1", "unist-util-visit": "^5.0.0" } }, "sha512-Als/wlYpXg2Rs0p/yofrkSH6IQzn1xh6cvBC5BHy5JVT3lGlzUvVT8wOyVqCEgf8Eun6cQ3f47rLLoaiyLkP0w=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], diff --git a/demo/another.md b/demo/another.md index 2792cae..2786cbc 100644 --- a/demo/another.md +++ b/demo/another.md @@ -17,3 +17,7 @@ How about a callout? > [!warning]- This is collapsible > And how! + +## A subheading + +This is a section! diff --git a/demo/index.md b/demo/index.md index 339807f..5830805 100644 --- a/demo/index.md +++ b/demo/index.md @@ -1,5 +1,6 @@ --- title: Home | Demo +and: how --- # Demo Data @@ -33,3 +34,8 @@ Tables should be a thing too - Need numbers HTML works, still! + + +| hello | world | hello | world | hello | world | hello | world | hello | world | hello | +| ------------------------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | +| dfjhasfdlkjahslfkjahsdlfjhasldjhasldjhasldk | | | | | | | | | | | diff --git a/package.json b/package.json index 05fcb90..a62542f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", + "chokidar": "^4.0.3", "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index 4299046..7e111a4 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -92,16 +92,19 @@ a { } table { - border-collapse: collapse; - width: 100%; - max-width: 100%; - margin: 1em 0; - font-size: 1em; - background-color: var(--color-bg); + display: block; color: var(--color-text); + background-color: var(--color-bg); + width: 100%; + max-width: -moz-fit-content; + max-width: fit-content; + overflow-x: auto; + font-size: 1em; border: 1px solid var(--color-text); + margin: 0 auto; + white-space: nowrap; + border-collapse: collapse; } - th, td { padding: 0.5em 0.5em; diff --git a/src/index.ts b/src/index.ts index ab83f80..3a6e9ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import Path from "node:path"; import { parseArgs } from "node:util"; import EMDY from "@endeavorance/emdy"; -import type { BunFile } from "bun"; +import { Glob, type BunFile } from "bun"; import DEFAULT_STYLESHEET from "./defaults/default-style.css" with { type: "text", }; @@ -10,10 +10,17 @@ import DEFAULT_TEMPLATE from "./defaults/default-template.html" with { }; import { CLIError } from "./error"; import { parseMarkdown } from "./markdown"; +import { watch } from "node:fs/promises"; const DEFAULT_TEMPLATE_FILE = "_template.html"; const DEFAULT_STYLESHEET_FILE = "_style.css"; +async function isDirectory(path: string): Promise { + const file = Bun.file(path); + const stat = await file.stat(); + return stat.isDirectory(); +} + /** * Given a file path, attempt to read the text of the file * @param filePath - The path to the file to read @@ -66,6 +73,9 @@ interface CLIOptions { /** The path to the stylesheet file */ stylesheetFilePath: string | null; + /** If the default stylesheet should be included regardless of the provided stylesheet */ + includeDefaultStylesheet: boolean; + /** If true, show help message */ help: boolean; @@ -107,6 +117,10 @@ function parseCLIArgs(): { options: CLIOptions; args: string[] } { type: "string", short: "s", }, + "include-default-stylesheet": { + type: "boolean", + short: "S", + }, cwd: { type: "string", default: process.cwd(), @@ -119,12 +133,14 @@ function parseCLIArgs(): { options: CLIOptions; args: string[] } { allowPositionals: true, }); - // Validation + // == Argument Validation == - if (positionals.length > 1 && flags.outfile) { + // Outfile requires only one argument + if (flags.outfile && positionals.length > 1) { throw new CLIError("--outfile cannot be used with multiple inputs."); } + // Outfile and Outdir cannot be used together if (flags.outdir && flags.outfile) { throw new CLIError("--outdir and --outfile cannot be used together."); } @@ -137,6 +153,7 @@ function parseCLIArgs(): { options: CLIOptions; args: string[] } { stdout: flags.stdout ?? false, templateFilePath: flags.template ?? null, stylesheetFilePath: flags.stylesheet ?? null, + includeDefaultStylesheet: flags["include-default-stylesheet"] ?? false, help: flags.help ?? false, title: flags.title ?? null, }, @@ -171,8 +188,11 @@ async function loadTemplate(options: CLIOptions): Promise { * @returns The stylesheet string */ async function loadStylesheet(options: CLIOptions): Promise { + const preamble = options.includeDefaultStylesheet ? DEFAULT_STYLESHEET : ""; + if (options.stylesheetFilePath) { - return readFile(options.stylesheetFilePath); + const loadedSheet = await readFile(options.stylesheetFilePath); + return [preamble, loadedSheet].join("\n"); } const checkStylesheetFile = Bun.file( @@ -180,7 +200,8 @@ async function loadStylesheet(options: CLIOptions): Promise { ); if (await checkStylesheetFile.exists()) { - return checkStylesheetFile.text(); + const loadedSheet = await checkStylesheetFile.text(); + return [preamble, loadedSheet].join("\n"); } return DEFAULT_STYLESHEET; @@ -263,6 +284,27 @@ async function getOutputFile( return Bun.file(outputPath); } +function processStdin(options: CLIOptions): Promise { + const outfile = options.outfile ? Bun.file(options.outfile) : Bun.stdout; + return buildFile(Bun.stdin, outfile, options); +} + +async function processFile( + options: CLIOptions, + filePath: string, +): Promise { + console.log(`Building ${filePath}...`); + const file = Bun.file(filePath); + + // Ensure input files exist + if (!(await file.exists())) { + throw new CLIError(`File ${filePath} does not exist.`); + } + + const outfile = await getOutputFile(filePath, options); + await buildFile(file, outfile, options); +} + async function main() { const { options, args } = parseCLIArgs(); @@ -277,13 +319,14 @@ Usage: echo "some markdown" | buildmd Options: - --outfile, -o Output path - --outdir, -d Output directory - --stdout Force output to stdout - --template, -t Template path (default: _template.html) - --stylesheet, -s Stylesheet path (default: _style.css) - --title, -T Document title override - --help, -h Show this help message + --outfile, -o Output path + --outdir, -d Output directory + --stdout Force output to stdout + --template, -t Template path (default: _template.html) + --stylesheet, -s Stylesheet path (default: _style.css) + --include-default-stylesheet, -S Extend default CSS instead of overwriting + --title, -T Document title override + --help, -h Show this help message `.trim(), ); process.exit(0); @@ -291,22 +334,21 @@ Options: // stdin mode if (args.length === 0) { - const outfile = options.outfile ? Bun.file(options.outfile) : Bun.stdout; - await buildFile(Bun.stdin, outfile, options); - process.exit(0); + return processStdin(options); } // file mode for (const arg of args) { - const file = Bun.file(arg); + const isDir = await isDirectory(arg); - // Ensure input files exist - if (!(await file.exists())) { - throw new CLIError(`File ${arg} does not exist.`); + if (isDir) { + const dirGlob = new Glob(`${arg}/**/*.md`); + for await (const file of dirGlob.scan()) { + await processFile(options, file); + } + } else { + await processFile(options, arg); } - - const outfile = await getOutputFile(arg, options); - await buildFile(file, outfile, options); } } From 5d5b2ffb6855867f246e7e8246f45a5814eb0655 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Sat, 7 Jun 2025 10:32:27 -0400 Subject: [PATCH 10/13] Remove unused dep --- bun.lock | 5 ----- package.json | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index a9dd4de..bb6a4c4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", - "chokidar": "^4.0.3", "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", @@ -73,8 +72,6 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -209,8 +206,6 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "rehype-callouts": ["rehype-callouts@2.1.0", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "hastscript": "^9.0.1", "unist-util-visit": "^5.0.0" } }, "sha512-Als/wlYpXg2Rs0p/yofrkSH6IQzn1xh6cvBC5BHy5JVT3lGlzUvVT8wOyVqCEgf8Eun6cQ3f47rLLoaiyLkP0w=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], diff --git a/package.json b/package.json index a62542f..68ed5b8 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "@types/bun": "latest" }, @@ -13,7 +13,6 @@ "dependencies": { "@biomejs/biome": "^1.9.4", "@endeavorance/emdy": "^1.0.0", - "chokidar": "^4.0.3", "rehype-callouts": "^2.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", From 6afc96f3326df28bf19e09fd432c59eae16b0873 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Sat, 7 Jun 2025 10:51:22 -0400 Subject: [PATCH 11/13] Support custom heading IDs --- bun.lock | 7 +++++++ demo/index.md | 2 +- package.json | 3 ++- src/markdown.ts | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index bb6a4c4..32d84ef 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", + "remark-custom-heading-id": "^2.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", @@ -120,6 +121,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "mdast-heading-id": ["mdast-heading-id@2.0.0", "", {}, "sha512-JVh0xKqsI3INPzJEqqCiuq3BGde5x6AlBTQAetLQQr9qTiDjX4dCvFhIiGmyPt1rftiHELQPI5obGphIOTXeWQ=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], @@ -172,6 +175,8 @@ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + "micromark-heading-id": ["micromark-heading-id@2.0.0", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-TdR5WCmwuCroK8QrfX0/4w9bjj3ayN0GzatxGV/lWQKmM75kNKVZEmdvKZS4MOskARxWXu1WHdIJjMSVMBegow=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], @@ -214,6 +219,8 @@ "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + "remark-custom-heading-id": ["remark-custom-heading-id@2.0.0", "", { "dependencies": { "mdast-heading-id": "*", "micromark-heading-id": "*", "unist-util-visit": "^5.0.0" } }, "sha512-LpR+c4pmuBGG/zhNbebecDnpKyXU4B7bdNu6tXW83T3QWlkwUpHYyUR8uXQ/BERK9HlI5pXGLu7DjkIS07/iVQ=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], diff --git a/demo/index.md b/demo/index.md index 5830805..1cbb734 100644 --- a/demo/index.md +++ b/demo/index.md @@ -17,7 +17,7 @@ Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapi Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. -## Another section +## Another section {#customid} Tables should be a thing too diff --git a/package.json b/package.json index 68ed5b8..2d67dd2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "0.3.0", + "version": "0.3.1", "devDependencies": { "@types/bun": "latest" }, @@ -17,6 +17,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", + "remark-custom-heading-id": "^2.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", diff --git a/src/markdown.ts b/src/markdown.ts index e03dd76..8d497e2 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -6,11 +6,13 @@ import remarkRehype from "remark-rehype"; import rehypeSlug from "rehype-slug"; import { unified } from "unified"; import rehypeCallouts from "rehype-callouts"; +import { remarkHeadingId } from "remark-custom-heading-id"; export async function parseMarkdown(markdown: string): Promise { const file = await unified() .use(remarkParse) .use(remarkGfm) + .use(remarkHeadingId) .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeRaw) .use(rehypeSlug) From 459f144bef1fade048fa7760bd8ba2ce774cad49 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Sat, 7 Jun 2025 11:33:38 -0400 Subject: [PATCH 12/13] Fix table whitespace --- demo/index.md | 2 +- src/defaults/default-style.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/index.md b/demo/index.md index 1cbb734..eca0e1c 100644 --- a/demo/index.md +++ b/demo/index.md @@ -38,4 +38,4 @@ Tables should be a thing too | hello | world | hello | world | hello | world | hello | world | hello | world | hello | | ------------------------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | -| dfjhasfdlkjahslfkjahsdlfjhasldjhasldjhasldk | | | | | | | | | | | +| dfjhasfdlkjahslfkjahsdlfjhasldjhasldjhasldk and such and things and whatnot and even more words in this cell so that hopefully itll wrap at some point ya dig? | | | | | | | | | | | diff --git a/src/defaults/default-style.css b/src/defaults/default-style.css index 7e111a4..d03160b 100644 --- a/src/defaults/default-style.css +++ b/src/defaults/default-style.css @@ -102,7 +102,6 @@ table { font-size: 1em; border: 1px solid var(--color-text); margin: 0 auto; - white-space: nowrap; border-collapse: collapse; } th, From 0dbba6f238c75872ae6ad4b08b7542f518ba5edd Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Sat, 7 Jun 2025 13:13:18 -0400 Subject: [PATCH 13/13] Restructure files --- package.json | 4 + src/build.ts | 42 ++++++ src/file-util.ts | 77 +++++++++++ src/index.ts | 334 +++------------------------------------------- src/markdown.ts | 30 ++++- src/options.ts | 128 ++++++++++++++++++ src/stylesheet.ts | 33 +++++ src/template.ts | 46 +++++++ 8 files changed, 375 insertions(+), 319 deletions(-) create mode 100644 src/build.ts create mode 100644 src/file-util.ts create mode 100644 src/options.ts create mode 100644 src/stylesheet.ts create mode 100644 src/template.ts diff --git a/package.json b/package.json index 2d67dd2..b14fa37 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,10 @@ "type": "module", "private": true, "version": "0.3.1", + "scripts": { + "build": ".run/build", + "fmt": "bunx --bun biome check --fix" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..cb8d5c9 --- /dev/null +++ b/src/build.ts @@ -0,0 +1,42 @@ +import EMDY from "@endeavorance/emdy"; +import type { BunFile } from "bun"; +import { parseMarkdown } from "./markdown"; +import type { CLIOptions } from "./options"; +import { loadStylesheet } from "./stylesheet"; +import { loadTemplate, renderTemplate } from "./template"; + +/** + * Build a file and return the rendered HTML string + * @param infile - The input file to read from + * @param outfile - The output file to write to + * @param options - The CLI options containing template and stylesheet paths + */ +export async function buildFile( + infile: BunFile, + options: CLIOptions, +): Promise { + // Load resources + const input = await infile.text(); + const template = await loadTemplate(options); + const stylesheet = await loadStylesheet(options); + const { content, ...props } = EMDY.parse(input) as Record & { + content: string; + }; + + // Render markdown + const html = await parseMarkdown(content); + + // Prepare data + const title = options.title ?? props.title ?? "Untitled"; + const templateData = { + content: html, + title, + stylesheet: stylesheet, + timestamp: new Date().toISOString(), + datetime: new Date().toLocaleString(), + ...props, + }; + + // Render template + return renderTemplate(template, templateData); +} diff --git a/src/file-util.ts b/src/file-util.ts new file mode 100644 index 0000000..f4a0d78 --- /dev/null +++ b/src/file-util.ts @@ -0,0 +1,77 @@ +import Path from "node:path"; +import type { BunFile } from "bun"; +import { CLIError } from "./error"; +import type { CLIOptions } from "./options"; + +/** + * Check if a given path is a directory + * + * @param path - The path to check + * @returns A promise that resolves to true if the path is a directory, false otherwise + */ +export async function isDirectory(path: string): Promise { + const file = Bun.file(path); + const stat = await file.stat(); + return stat.isDirectory(); +} + +/** + * Given a file path, attempt to read the text of the file + * @param filePath - The path to the file to read + * @returns The text content of the file + * + * @throws CLIError if the file does not exist + */ +export async function readFile(filePath: string): Promise { + const file = Bun.file(filePath); + const exists = await file.exists(); + if (!exists) { + throw new CLIError(`File ${filePath} does not exist.`); + } + return file.text(); +} + +/** + * Get the output path based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output directory + * @returns The resolved output path + */ +async function getOutputPath(inputPath: string, options: CLIOptions) { + const inputDirname = Path.dirname(inputPath); + const inputBasename = Path.basename(inputPath); + const outputBasename = inputBasename.replace(/\.md$/, ".html"); + + if (options.outdir) { + const dirPath = Path.relative(options.cwd, inputDirname); + const outputPath = Path.join(options.outdir, dirPath, outputBasename); + return Path.resolve(outputPath); + } + + return Path.join(inputDirname, outputBasename); +} + +/** + * Get the output file based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output file and directory + * @returns The BunFile object representing the output file + */ +export async function getOutputFile( + inputPath: string, + options: CLIOptions, +): Promise { + // --stdout option -> Force stdout + if (options.stdout) { + return Bun.stdout; + } + + // Exact outfile defined -> write to file + if (options.outfile) { + return Bun.file(options.outfile); + } + + // Input path with --outdir -> calculate output path + const outputPath = await getOutputPath(inputPath, options); + return Bun.file(outputPath); +} diff --git a/src/index.ts b/src/index.ts index 3a6e9ba..e6bee9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,297 +1,18 @@ -import Path from "node:path"; -import { parseArgs } from "node:util"; -import EMDY from "@endeavorance/emdy"; -import { Glob, type BunFile } from "bun"; -import DEFAULT_STYLESHEET from "./defaults/default-style.css" with { - type: "text", -}; -import DEFAULT_TEMPLATE from "./defaults/default-template.html" with { - type: "text", -}; +import { Glob } from "bun"; +import { buildFile } from "./build"; import { CLIError } from "./error"; -import { parseMarkdown } from "./markdown"; -import { watch } from "node:fs/promises"; +import { getOutputFile, isDirectory } from "./file-util"; +import { type CLIOptions, USAGE, parseCLIArgs } from "./options"; -const DEFAULT_TEMPLATE_FILE = "_template.html"; -const DEFAULT_STYLESHEET_FILE = "_style.css"; - -async function isDirectory(path: string): Promise { - const file = Bun.file(path); - const stat = await file.stat(); - return stat.isDirectory(); -} - -/** - * Given a file path, attempt to read the text of the file - * @param filePath - The path to the file to read - * @returns The text content of the file - * - * @throws CLIError if the file does not exist - */ -async function readFile(filePath: string): Promise { - const file = Bun.file(filePath); - const exists = await file.exists(); - if (!exists) { - throw new CLIError(`File ${filePath} does not exist.`); - } - return file.text(); -} - -/** - * Renders the provided template with the given data. - * @param template - The template string containing placeholders like %key% - * @param data - An object containing key-value pairs to replace in the template - * @returns The rendered string with all placeholders replaced by their corresponding values - */ -function renderTemplate( - template: string, - data: Record, -): string { - let output = template; - for (const [key, value] of Object.entries(data)) { - output = output.replace(new RegExp(`%${key}%`, "g"), String(value)); - } - return output; -} - -/** - * A collection of options parsed from the command line arguments. - */ -interface CLIOptions { - /** A path to the output file */ - outfile: string | null; - - /** A path to the output directory */ - outdir: string | null; - - /** If true, force output to stdout */ - stdout: boolean; - - /** The path to the template file */ - templateFilePath: string | null; - - /** The path to the stylesheet file */ - stylesheetFilePath: string | null; - - /** If the default stylesheet should be included regardless of the provided stylesheet */ - includeDefaultStylesheet: boolean; - - /** If true, show help message */ - help: boolean; - - /** If provided, overrides the document title */ - title: string | null; - - /** If provided, overrides the current working directory (default: .) */ - cwd: string; -} - -/** - * Parse the command line arguments and return the options and positional arguments. - * @returns An object containing the parsed options and positional arguments - */ -function parseCLIArgs(): { options: CLIOptions; args: string[] } { - const { values: flags, positionals } = parseArgs({ - args: Bun.argv.slice(2), - options: { - outfile: { - type: "string", - short: "o", - }, - outdir: { - type: "string", - short: "d", - }, - stdout: { - type: "boolean", - }, - template: { - type: "string", - short: "t", - }, - title: { - type: "string", - short: "T", - }, - stylesheet: { - type: "string", - short: "s", - }, - "include-default-stylesheet": { - type: "boolean", - short: "S", - }, - cwd: { - type: "string", - default: process.cwd(), - }, - help: { - type: "boolean", - short: "h", - }, - }, - allowPositionals: true, - }); - - // == Argument Validation == - - // Outfile requires only one argument - if (flags.outfile && positionals.length > 1) { - throw new CLIError("--outfile cannot be used with multiple inputs."); - } - - // Outfile and Outdir cannot be used together - if (flags.outdir && flags.outfile) { - throw new CLIError("--outdir and --outfile cannot be used together."); - } - - return { - options: { - cwd: flags.cwd, - outfile: flags.outfile ?? null, - outdir: flags.outdir ?? null, - stdout: flags.stdout ?? false, - templateFilePath: flags.template ?? null, - stylesheetFilePath: flags.stylesheet ?? null, - includeDefaultStylesheet: flags["include-default-stylesheet"] ?? false, - help: flags.help ?? false, - title: flags.title ?? null, - }, - args: positionals, - }; -} - -/** - * Attempt to load a custom template, falling back to a default - * @param options - The CLI options containing the template file path - * @returns The template string - */ -async function loadTemplate(options: CLIOptions): Promise { - if (options.templateFilePath) { - return readFile(options.templateFilePath); - } - - const checkTemplateFile = Bun.file( - Path.join(options.cwd, DEFAULT_TEMPLATE_FILE), - ); - - if (await checkTemplateFile.exists()) { - return checkTemplateFile.text(); - } - - return DEFAULT_TEMPLATE; -} - -/** - * Attempt to load a custom stylesheet, falling back to a default - * @param options - The CLI options containing the stylesheet file path - * @returns The stylesheet string - */ -async function loadStylesheet(options: CLIOptions): Promise { - const preamble = options.includeDefaultStylesheet ? DEFAULT_STYLESHEET : ""; - - if (options.stylesheetFilePath) { - const loadedSheet = await readFile(options.stylesheetFilePath); - return [preamble, loadedSheet].join("\n"); - } - - const checkStylesheetFile = Bun.file( - Path.join(options.cwd, DEFAULT_STYLESHEET_FILE), - ); - - if (await checkStylesheetFile.exists()) { - const loadedSheet = await checkStylesheetFile.text(); - return [preamble, loadedSheet].join("\n"); - } - - return DEFAULT_STYLESHEET; -} - -/** - * Build and write a file - * @param infile - The input file to read from - * @param outfile - The output file to write to - * @param options - The CLI options containing template and stylesheet paths - */ -async function buildFile( - infile: BunFile, - outfile: BunFile, - options: CLIOptions, -): Promise { - const input = await infile.text(); - const template = await loadTemplate(options); - const stylesheet = await loadStylesheet(options); - const { content, ...props } = EMDY.parse(input) as Record & { - content: string; - }; - - const html = await parseMarkdown(content); - const title = options.title ?? props.title ?? "Untitled"; - const templateData = { - content: html, - title, - stylesheet: stylesheet, - timestamp: new Date().toISOString(), - datetime: new Date().toLocaleString(), - ...props, - }; - - await Bun.write(outfile, renderTemplate(template, templateData)); -} - -/** - * Get the output path based on the input path and options - * @param inputPath - The path of the input file - * @param options - The CLI options containing output directory - * @returns The resolved output path - */ -async function getOutputPath(inputPath: string, options: CLIOptions) { - const inputDirname = Path.dirname(inputPath); - const inputBasename = Path.basename(inputPath); - const outputBasename = inputBasename.replace(/\.md$/, ".html"); - - if (options.outdir) { - const dirPath = Path.relative(options.cwd, inputDirname); - const outputPath = Path.join(options.outdir, dirPath, outputBasename); - return Path.resolve(outputPath); - } - - return Path.join(inputDirname, outputBasename); -} - -/** - * Get the output file based on the input path and options - * @param inputPath - The path of the input file - * @param options - The CLI options containing output file and directory - * @returns The BunFile object representing the output file - */ -async function getOutputFile( - inputPath: string, - options: CLIOptions, -): Promise { - // --stdout option -> Force stdout - if (options.stdout) { - return Bun.stdout; - } - - // Exact outfile defined -> write to file - if (options.outfile) { - return Bun.file(options.outfile); - } - - // Input path with --outdir -> calculate output path - const outputPath = await getOutputPath(inputPath, options); - return Bun.file(outputPath); -} - -function processStdin(options: CLIOptions): Promise { +async function processStdin(options: CLIOptions): Promise { const outfile = options.outfile ? Bun.file(options.outfile) : Bun.stdout; - return buildFile(Bun.stdin, outfile, options); + const renderedHTML = await buildFile(Bun.stdin, options); + await Bun.write(outfile, renderedHTML); } -async function processFile( - options: CLIOptions, +async function processFileAtPath( filePath: string, + options: CLIOptions, ): Promise { console.log(`Building ${filePath}...`); const file = Bun.file(filePath); @@ -302,34 +23,16 @@ async function processFile( } const outfile = await getOutputFile(filePath, options); - await buildFile(file, outfile, options); + const renderedHTML = await buildFile(file, options); + await Bun.write(outfile, renderedHTML); } async function main() { const { options, args } = parseCLIArgs(); if (options.help) { - console.log( - ` -buildmd - Build markdown files to html with templates and metadata - -Usage: - buildmd file.md - buildmd file.md -o output.html - echo "some markdown" | buildmd - -Options: - --outfile, -o Output path - --outdir, -d Output directory - --stdout Force output to stdout - --template, -t Template path (default: _template.html) - --stylesheet, -s Stylesheet path (default: _style.css) - --include-default-stylesheet, -S Extend default CSS instead of overwriting - --title, -T Document title override - --help, -h Show this help message -`.trim(), - ); - process.exit(0); + console.log(USAGE); + return; } // stdin mode @@ -338,22 +41,23 @@ Options: } // file mode - for (const arg of args) { - const isDir = await isDirectory(arg); + for (const inputPath of args) { + const isDir = await isDirectory(inputPath); if (isDir) { - const dirGlob = new Glob(`${arg}/**/*.md`); + const dirGlob = new Glob(`${inputPath}/**/*.md`); for await (const file of dirGlob.scan()) { - await processFile(options, file); + await processFileAtPath(file, options); } } else { - await processFile(options, arg); + await processFileAtPath(inputPath, options); } } } try { await main(); + process.exit(0); } catch (error) { if (error instanceof CLIError) { console.error(error.message); diff --git a/src/markdown.ts b/src/markdown.ts index 8d497e2..3e9aef2 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -1,22 +1,44 @@ +import rehypeCallouts from "rehype-callouts"; import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; +import rehypeSlug from "rehype-slug"; import rehypeStringify from "rehype-stringify"; +import { remarkHeadingId } from "remark-custom-heading-id"; +import remarkGfm from "remark-gfm"; import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; -import rehypeSlug from "rehype-slug"; import { unified } from "unified"; -import rehypeCallouts from "rehype-callouts"; -import { remarkHeadingId } from "remark-custom-heading-id"; +/** + * Parse the provided markdown into an HTML string + * + * @param markdown - The markdown string to parse + * @returns The parsed HTML string in a promise + */ export async function parseMarkdown(markdown: string): Promise { const file = await unified() + + // Parse markdown .use(remarkParse) + + // Support github-flavored markdown .use(remarkGfm) + + // Accept custom heading ids: # My heading {#custom-id} .use(remarkHeadingId) + + // Pass-thru HTML .use(remarkRehype, { allowDangerousHtml: true }) + + // Process to html/html .use(rehypeRaw) + + // Apply heading IDs to headings without a custom ID .use(rehypeSlug) + + // Support callouts/admonitions .use(rehypeCallouts) + + // Render to string .use(rehypeStringify) .process(markdown); diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..d0b6f3e --- /dev/null +++ b/src/options.ts @@ -0,0 +1,128 @@ +import { parseArgs } from "node:util"; +import { CLIError } from "./error"; + +/** + * A collection of options parsed from the command line arguments. + */ +export interface CLIOptions { + /** A path to the output file */ + outfile: string | null; + + /** A path to the output directory */ + outdir: string | null; + + /** If true, force output to stdout */ + stdout: boolean; + + /** The path to the template file */ + templateFilePath: string | null; + + /** The path to the stylesheet file */ + stylesheetFilePath: string | null; + + /** If the default stylesheet should be included regardless of the provided stylesheet */ + includeDefaultStylesheet: boolean; + + /** If true, show help message */ + help: boolean; + + /** If provided, overrides the document title */ + title: string | null; + + /** If provided, overrides the current working directory (default: .) */ + cwd: string; +} + +/** + * Parse the command line arguments and return the options and positional arguments. + * @returns An object containing the parsed options and positional arguments + */ +export function parseCLIArgs(): { options: CLIOptions; args: string[] } { + const { values: flags, positionals } = parseArgs({ + args: Bun.argv.slice(2), + options: { + outfile: { + type: "string", + short: "o", + }, + outdir: { + type: "string", + short: "d", + }, + stdout: { + type: "boolean", + }, + template: { + type: "string", + short: "t", + }, + title: { + type: "string", + short: "T", + }, + stylesheet: { + type: "string", + short: "s", + }, + "include-default-stylesheet": { + type: "boolean", + short: "S", + }, + cwd: { + type: "string", + default: process.cwd(), + }, + help: { + type: "boolean", + short: "h", + }, + }, + allowPositionals: true, + }); + + // == Argument Validation == + + // Outfile requires only one argument + if (flags.outfile && positionals.length > 1) { + throw new CLIError("--outfile cannot be used with multiple inputs."); + } + + // Outfile and Outdir cannot be used together + if (flags.outdir && flags.outfile) { + throw new CLIError("--outdir and --outfile cannot be used together."); + } + + return { + options: { + cwd: flags.cwd, + outfile: flags.outfile ?? null, + outdir: flags.outdir ?? null, + stdout: flags.stdout ?? false, + templateFilePath: flags.template ?? null, + stylesheetFilePath: flags.stylesheet ?? null, + includeDefaultStylesheet: flags["include-default-stylesheet"] ?? false, + help: flags.help ?? false, + title: flags.title ?? null, + }, + args: positionals, + }; +} + +export const USAGE = ` +buildmd - Build markdown files to html with templates and metadata + +Usage: + buildmd file.md + buildmd file.md -o output.html + echo "some markdown" | buildmd + +Options: + --outfile, -o Output path + --outdir, -d Output directory + --stdout Force output to stdout + --template, -t Template path (default: _template.html) + --stylesheet, -s Stylesheet path (default: _style.css) + --include-default-stylesheet, -S Extend default CSS instead of overwriting + --title, -T Document title override + --help, -h Show this help message +`.trim(); diff --git a/src/stylesheet.ts b/src/stylesheet.ts new file mode 100644 index 0000000..595bae3 --- /dev/null +++ b/src/stylesheet.ts @@ -0,0 +1,33 @@ +import Path from "node:path"; +import DEFAULT_STYLESHEET from "./defaults/default-style.css" with { + type: "text", +}; +import { readFile } from "./file-util"; +import type { CLIOptions } from "./options"; + +const DEFAULT_STYLESHEET_FILE = "_style.css"; + +/** + * Attempt to load a custom stylesheet, falling back to a default + * @param options - The CLI options containing the stylesheet file path + * @returns The stylesheet string + */ +export async function loadStylesheet(options: CLIOptions): Promise { + const preamble = options.includeDefaultStylesheet ? DEFAULT_STYLESHEET : ""; + + if (options.stylesheetFilePath) { + const loadedSheet = await readFile(options.stylesheetFilePath); + return [preamble, loadedSheet].join("\n"); + } + + const checkStylesheetFile = Bun.file( + Path.join(options.cwd, DEFAULT_STYLESHEET_FILE), + ); + + if (await checkStylesheetFile.exists()) { + const loadedSheet = await checkStylesheetFile.text(); + return [preamble, loadedSheet].join("\n"); + } + + return DEFAULT_STYLESHEET; +} diff --git a/src/template.ts b/src/template.ts new file mode 100644 index 0000000..c340a1a --- /dev/null +++ b/src/template.ts @@ -0,0 +1,46 @@ +import Path from "node:path"; +import DEFAULT_TEMPLATE from "./defaults/default-template.html" with { + type: "text", +}; +import { readFile } from "./file-util"; +import type { CLIOptions } from "./options"; + +const DEFAULT_TEMPLATE_FILE = "_template.html"; + +/** + * Renders the provided template with the given data. + * @param template - The template string containing placeholders like %key% + * @param data - An object containing key-value pairs to replace in the template + * @returns The rendered string with all placeholders replaced by their corresponding values + */ +export function renderTemplate( + template: string, + data: Record, +): string { + let output = template; + for (const [key, value] of Object.entries(data)) { + output = output.replace(new RegExp(`%${key}%`, "g"), String(value)); + } + return output; +} + +/** + * Attempt to load a custom template, falling back to a default + * @param options - The CLI options containing the template file path + * @returns The template string + */ +export async function loadTemplate(options: CLIOptions): Promise { + if (options.templateFilePath) { + return readFile(options.templateFilePath); + } + + const checkTemplateFile = Bun.file( + Path.join(options.cwd, DEFAULT_TEMPLATE_FILE), + ); + + if (await checkTemplateFile.exists()) { + return checkTemplateFile.text(); + } + + return DEFAULT_TEMPLATE; +}