diff --git a/.github/workflows/deploy-site.yaml b/.github/workflows/deploy-site.yaml index faf158a..ea2c046 100644 --- a/.github/workflows/deploy-site.yaml +++ b/.github/workflows/deploy-site.yaml @@ -15,7 +15,7 @@ jobs: with: node-version: 22.9.0 - name: Build site - run: make build-server + run: make build-docs - name: Deploy site uses: JamesIves/github-pages-deploy-action@v4.4.3 diff --git a/Makefile b/Makefile index 7e72407..a35059c 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,12 @@ dev-server: @echo "Start dev server" ./node_modules/.bin/esbuild $(FLAGS) --define:LIVE_RELOAD=true --watch --servedir=./display -build-server: - @echo "Build server" - ./node_modules/.bin/esbuild $(FLAGS) --define:LIVE_RELOAD=false - lint: @echo "Lint" ./node_modules/.bin/eslint --fix . + +dev-docs: + ./node_modules/.bin/tsx scripts/serve.ts + +build-docs: + ./node_modules/.bin/tsx scripts/render.ts diff --git a/dev/index.html b/dev/index.html index 94cdb3f..4ea85ca 100644 --- a/dev/index.html +++ b/dev/index.html @@ -3,7 +3,7 @@ - Document + Squarified Example + + +
+ + + + - pre.js: | + // pre.js + import { createTreemap, presetDecorator } from 'squarified' + const data = [{ + name: 'root', + weight: 100, + groups: [ + { name: 'a', weight: 10 }, + { name: 'b', weight: 20 }, + { name: 'c', weight: 30 }, + { name: 'd', weight: 40 }, + ] + }] + const treemap = createTreemap() + treemap.use('decorator', presetDecorator()) + const el = document.getElementById('app') + treemap.init(el) + treemap.setOptions({ data }) diff --git a/docs/index.yaml b/docs/index.yaml new file mode 100644 index 0000000..f1810c2 --- /dev/null +++ b/docs/index.yaml @@ -0,0 +1,18 @@ +index: + title: A minimal and powerful treemap component. + body: + - h1: Squarified + - p: A treemap layout algorithm that optimizes for aspect ratio. + + - p: "Major features:" + - ul: + - "Minimalistic API" + - "Powerful layout algorithm(Based on Squarified Treemaps)" + - "Highly customizable" + - "Supports zooming and panning" + + - p: > + Check out the [getting started](/getting-started) guide to learn how to use. + +getting-started: "getting-started.yaml" +api: "api.yaml" diff --git a/global.d.ts b/global.d.ts index 875c898..4d565bd 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,2 +1,4 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any type Any = any + +type AnyObject = Record diff --git a/package.json b/package.json index 6bb6f8b..2b76559 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,22 @@ "author": "Kanno", "license": "MIT", "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/markdown-it": "^14.1.2", "@types/node": "^22.7.4", + "chokidar": "^4.0.3", "dprint": "^0.47.2", "esbuild": "^0.24.0", "eslint": "^9.16.0", "eslint-config-kagura": "^3.0.1", + "highlight.js": "^11.11.1", "jiek": "^2.3.3", + "js-yaml": "^4.1.0", + "markdown-it": "^14.1.0", + "tinyexec": "^0.3.2", + "tsx": "^4.19.2", "typescript": "^5.7.3", - "vite-bundle-analyzer": "^0.16.0" + "vite-bundle-analyzer": "^0.16.2" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ad8d70..348841f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,9 +12,18 @@ overrides: importers: .: devDependencies: + "@types/js-yaml": + specifier: ^4.0.9 + version: 4.0.9 + "@types/markdown-it": + specifier: ^14.1.2 + version: 14.1.2 "@types/node": specifier: ^22.7.4 version: 22.7.4 + chokidar: + specifier: ^4.0.3 + version: 4.0.3 dprint: specifier: ^0.47.2 version: 0.47.2 @@ -27,15 +36,30 @@ importers: eslint-config-kagura: specifier: ^3.0.1 version: 3.0.1(@typescript-eslint/eslint-plugin@8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0)(typescript@5.7.3))(eslint@9.16.0)(typescript@5.7.3))(eslint@9.16.0)(typescript@5.7.3) + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 jiek: specifier: ^2.3.3 - version: 2.3.3(@pnpm/filter-workspace-packages@10.0.14)(@rollup/plugin-terser@0.4.4(rollup@4.13.2))(@types/node@22.7.4)(esbuild-register@3.6.0(esbuild@0.24.0))(postcss@8.4.47)(rollup-plugin-esbuild@6.1.1(esbuild@0.24.0)(rollup@4.13.2))(rollup-plugin-postcss@4.0.2(postcss@8.4.47))(typescript@5.7.3)(vite-bundle-analyzer@0.16.0) + version: 2.3.3(@pnpm/filter-workspace-packages@10.0.14)(@rollup/plugin-terser@0.4.4(rollup@4.13.2))(@types/node@22.7.4)(esbuild-register@3.6.0(esbuild@0.24.0))(postcss@8.4.47)(rollup-plugin-esbuild@6.1.1(esbuild@0.24.0)(rollup@4.13.2))(rollup-plugin-postcss@4.0.2(postcss@8.4.47))(typescript@5.7.3)(vite-bundle-analyzer@0.16.2) + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 + tinyexec: + specifier: ^0.3.2 + version: 0.3.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.7.3 version: 5.7.3 vite-bundle-analyzer: - specifier: ^0.16.0 - version: 0.16.0 + specifier: ^0.16.2 + version: 0.16.2 packages: "@babel/code-frame@7.26.2": @@ -787,9 +811,21 @@ packages: "@types/estree@1.0.6": resolution: { integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== } + "@types/js-yaml@4.0.9": + resolution: { integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== } + "@types/json-schema@7.0.15": resolution: { integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== } + "@types/linkify-it@5.0.0": + resolution: { integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== } + + "@types/markdown-it@14.1.2": + resolution: { integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== } + + "@types/mdurl@2.0.0": + resolution: { integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== } + "@types/node@22.7.4": resolution: { integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== } @@ -1013,6 +1049,10 @@ packages: chardet@0.7.0: resolution: { integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== } + chokidar@4.0.3: + resolution: { integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== } + engines: { node: ">= 14.16.0" } + cli-boxes@2.2.1: resolution: { integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== } engines: { node: ">=6" } @@ -1231,6 +1271,10 @@ packages: entities@2.2.0: resolution: { integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== } + entities@4.5.0: + resolution: { integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== } + engines: { node: ">=0.12" } + error-ex@1.3.2: resolution: { integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== } @@ -1473,6 +1517,10 @@ packages: resolution: { integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== } engines: { node: ">= 0.4" } + highlight.js@11.11.1: + resolution: { integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w== } + engines: { node: ">=12.0.0" } + hosted-git-info@4.1.0: resolution: { integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== } engines: { node: ">=10" } @@ -1679,6 +1727,9 @@ packages: lines-and-columns@1.2.4: resolution: { integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== } + linkify-it@5.0.0: + resolution: { integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== } + load-json-file@6.2.0: resolution: { integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== } engines: { node: ">=8" } @@ -1724,6 +1775,10 @@ packages: resolution: { integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== } engines: { node: ">=8" } + markdown-it@14.1.0: + resolution: { integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== } + hasBin: true + math-intrinsics@1.1.0: resolution: { integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== } engines: { node: ">= 0.4" } @@ -1731,6 +1786,9 @@ packages: mdn-data@2.0.14: resolution: { integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== } + mdurl@2.0.0: + resolution: { integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== } + media-typer@0.3.0: resolution: { integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== } engines: { node: ">= 0.6" } @@ -2172,6 +2230,10 @@ packages: proto-list@1.2.4: resolution: { integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== } + punycode.js@2.3.1: + resolution: { integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== } + engines: { node: ">=6" } + punycode@2.3.1: resolution: { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== } engines: { node: ">=6" } @@ -2194,6 +2256,10 @@ packages: resolution: { integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ== } engines: { node: ">=10.13" } + readdirp@4.1.1: + resolution: { integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw== } + engines: { node: ">= 14.18.0" } + realpath-missing@1.1.0: resolution: { integrity: sha512-wnWtnywepjg/eHIgWR97R7UuM5i+qHLA195qdN9UPKvcMqfn60+67S8sPPW3vDlSEfYHoFkKU8IvpCNty3zQvQ== } engines: { node: ">=10" } @@ -2393,6 +2459,9 @@ packages: engines: { node: ">=10" } hasBin: true + tinyexec@0.3.2: + resolution: { integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== } + tmp@0.0.33: resolution: { integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== } engines: { node: ">=0.6.0" } @@ -2418,6 +2487,11 @@ packages: resolution: { integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== } engines: { node: ">=0.6.x" } + tsx@4.19.2: + resolution: { integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== } + engines: { node: ">=18.0.0" } + hasBin: true + type-check@0.4.0: resolution: { integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== } engines: { node: ">= 0.8.0" } @@ -2453,6 +2527,9 @@ packages: engines: { node: ">=14.17" } hasBin: true + uc.micro@2.1.0: + resolution: { integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== } + undici-types@6.19.8: resolution: { integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== } @@ -2492,8 +2569,8 @@ packages: resolution: { integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA== } engines: { node: ">=10.13" } - vite-bundle-analyzer@0.16.0: - resolution: { integrity: sha512-zJEXZuwqoNxBwuCLzpVLrlT3V7RPaPTlqkcHKJMaS98QIOuBMZ10XCeWQ11+lG4wc151KNkAP5xbxsp2kD+rOg== } + vite-bundle-analyzer@0.16.2: + resolution: { integrity: sha512-PoeF4Bm4ad21W/T7ZVP+vi+juR1GdBSUXUaAsE6Lyux76MoWKKRTKNsV6DGKSGUDwlIYIhlt6T5LI7e+UlxrhA== } hasBin: true wcwidth@1.0.1: @@ -3398,8 +3475,19 @@ snapshots: "@types/estree@1.0.6": {} + "@types/js-yaml@4.0.9": {} + "@types/json-schema@7.0.15": {} + "@types/linkify-it@5.0.0": {} + + "@types/markdown-it@14.1.2": + dependencies: + "@types/linkify-it": 5.0.0 + "@types/mdurl": 2.0.0 + + "@types/mdurl@2.0.0": {} + "@types/node@22.7.4": dependencies: undici-types: 6.19.8 @@ -3671,6 +3759,10 @@ snapshots: chardet@0.7.0: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.1 + cli-boxes@2.2.1: optional: true @@ -3917,6 +4009,8 @@ snapshots: entities@2.2.0: optional: true + entities@4.5.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -4179,7 +4273,6 @@ snapshots: get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 - optional: true glob-parent@5.1.2: dependencies: @@ -4213,6 +4306,8 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight.js@11.11.1: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -4322,7 +4417,7 @@ snapshots: isexe@2.0.0: {} - jiek@2.3.3(@pnpm/filter-workspace-packages@10.0.14)(@rollup/plugin-terser@0.4.4(rollup@4.13.2))(@types/node@22.7.4)(esbuild-register@3.6.0(esbuild@0.24.0))(postcss@8.4.47)(rollup-plugin-esbuild@6.1.1(esbuild@0.24.0)(rollup@4.13.2))(rollup-plugin-postcss@4.0.2(postcss@8.4.47))(typescript@5.7.3)(vite-bundle-analyzer@0.16.0): + jiek@2.3.3(@pnpm/filter-workspace-packages@10.0.14)(@rollup/plugin-terser@0.4.4(rollup@4.13.2))(@types/node@22.7.4)(esbuild-register@3.6.0(esbuild@0.24.0))(postcss@8.4.47)(rollup-plugin-esbuild@6.1.1(esbuild@0.24.0)(rollup@4.13.2))(rollup-plugin-postcss@4.0.2(postcss@8.4.47))(typescript@5.7.3)(vite-bundle-analyzer@0.16.2): dependencies: "@inquirer/prompts": 7.2.3(@types/node@22.7.4) "@jiek/pkger": 0.2.2 @@ -4348,7 +4443,7 @@ snapshots: rollup-plugin-esbuild: 6.1.1(esbuild@0.24.0)(rollup@4.13.2) rollup-plugin-postcss: 4.0.2(postcss@8.4.47) typescript: 5.7.3 - vite-bundle-analyzer: 0.16.0 + vite-bundle-analyzer: 0.16.2 transitivePeerDependencies: - "@types/node" - supports-color @@ -4435,6 +4530,10 @@ snapshots: lines-and-columns@1.2.4: optional: true + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + load-json-file@6.2.0: dependencies: graceful-fs: 4.2.11 @@ -4484,11 +4583,22 @@ snapshots: map-obj@4.3.0: optional: true + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} mdn-data@2.0.14: optional: true + mdurl@2.0.0: {} + media-typer@0.3.0: {} mem@6.1.1: @@ -4941,6 +5051,8 @@ snapshots: proto-list@1.2.4: optional: true + punycode.js@2.3.1: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4965,6 +5077,8 @@ snapshots: strip-bom: 4.0.0 optional: true + readdirp@4.1.1: {} + realpath-missing@1.1.0: optional: true @@ -4979,8 +5093,7 @@ snapshots: resolve-from@5.0.0: optional: true - resolve-pkg-maps@1.0.0: - optional: true + resolve-pkg-maps@1.0.0: {} resolve@1.22.8: dependencies: @@ -5211,6 +5324,8 @@ snapshots: source-map-support: 0.5.21 optional: true + tinyexec@0.3.2: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -5230,6 +5345,13 @@ snapshots: tsscmp@1.0.6: {} + tsx@4.19.2: + dependencies: + esbuild: 0.24.0 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -5260,6 +5382,8 @@ snapshots: typescript@5.7.3: {} + uc.micro@2.1.0: {} + undici-types@6.19.8: {} unique-string@2.0.0: @@ -5301,7 +5425,7 @@ snapshots: semver: 7.6.3 optional: true - vite-bundle-analyzer@0.16.0: {} + vite-bundle-analyzer@0.16.2: {} wcwidth@1.0.1: dependencies: diff --git a/scripts/live-reload.js b/scripts/live-reload.js new file mode 100644 index 0000000..bb1eb23 --- /dev/null +++ b/scripts/live-reload.js @@ -0,0 +1,3 @@ +new EventSource('/nonzzz').addEventListener('change', () => { + location.reload() +}) diff --git a/scripts/render.ts b/scripts/render.ts new file mode 100644 index 0000000..814612a --- /dev/null +++ b/scripts/render.ts @@ -0,0 +1,375 @@ +import esbuild from 'esbuild' +import fs from 'fs' +import fsp from 'fs/promises' +import hljs from 'highlight.js' +import yaml from 'js-yaml' +import markdownit from 'markdown-it' +import path from 'path' +import type { Theme } from './theme' + +const md = markdownit({ html: true }) + +const docsDir = path.join(__dirname, '..', 'docs') + +const devDir = path.join(__dirname, '..', 'dev') + +const destDir = path.join(__dirname, '..', 'display') + +const scriptDir = __dirname + +const target = ['chrome58', 'safari11', 'firefox57', 'edge16'] + +type TagElement = 'p' | 'ul' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'pre' | `pre.${string}` + +type TagValue = T extends 'ul' ? string[] : string + +interface FormattedTag { + tag: T + value: TagValue +} + +type AnyFormattedTag = FormattedTag + +interface RenderMetadata { + title: string + body: AnyFormattedTag[] +} + +const data = yaml.load(fs.readFileSync(path.join(docsDir, 'index.yaml'), 'utf8')) as Record + +const pages = Object.entries(data) + +function createTag(tag: T, value: TagValue): FormattedTag { + return { tag, value } +} + +export interface Descriptor { + kind: 'script' | 'style' | 'title' + text: string + attrs?: string[] +} + +interface InjectHTMLTagOptions { + html: string + injectTo: 'body' | 'head' + descriptors: Descriptor | Descriptor[] +} + +// Refactor this function +export function injectHTMLTag(options: InjectHTMLTagOptions) { + const regExp = options.injectTo === 'head' ? /([ \t]*)<\/head>/i : /([ \t]*)<\/body>/i + options.descriptors = Array.isArray(options.descriptors) ? options.descriptors : [options.descriptors] + const descriptors = options.descriptors.map((d) => { + if (d.attrs && d.attrs.length > 0) { + return `<${d.kind} ${d.attrs.join(' ')}>${d.text}` + } + return `<${d.kind}>${d.text}` + }) + return options.html.replace(regExp, (match) => `${descriptors.join('\n')}${match}`) +} + +function minifyCSS(css: string) { + return esbuild.transformSync(css, { target, loader: 'css', minify: true }).code +} + +function minifyJS(js: string) { + return esbuild.transformSync(js, { target, loader: 'ts', minify: true }).code +} + +function buildAndMinifyJS(entry: string) { + const r = esbuild.buildSync({ + bundle: true, + format: 'iife', + loader: { + '.ts': 'ts' + }, + define: { + LIVE_RELOAD: 'false' + }, + minify: true, + write: false, + entryPoints: [entry] + }) + if (r.outputFiles.length) { + return r.outputFiles[0].text + } + throw new Error('No output files') +} + +const formatedPages = pages.reduce((acc, [page, pageData]) => { + if (typeof pageData === 'string') { + if (pageData.endsWith('.yaml')) { + pageData = yaml.load(fs.readFileSync(path.join(docsDir, pageData), 'utf8')) as RenderMetadata + } + } + if (typeof pageData === 'object') { + pageData.body = pageData.body.map((sec) => { + const tag = Object.keys(sec)[0] + // @ts-expect-error safe + return createTag(tag as TagElement, sec[tag] as TagValue) + }) + } + // @ts-expect-error safe + acc.push([page, pageData]) + return acc +}, [] as [string, RenderMetadata][]) + +const hljsPath = path.dirname(require.resolve('highlight.js/package.json', { paths: [process.cwd()] })) + +// We use github highlighting style + +function pipeOriginalCSSIntoThemeSystem(css: string, theme: Theme) { + let wrappered = '' + if (theme === 'dark') { + wrappered = `html[data-theme="dark"] { ${css} }\n` + } else { + wrappered = `html:not([data-theme="dark"]) { ${css} }\n` + } + + return minifyCSS(wrappered) +} + +const hljsGithubCSS = { + light: pipeOriginalCSSIntoThemeSystem(fs.readFileSync(path.join(hljsPath, 'styles/github.css'), 'utf-8'), 'light'), + dark: pipeOriginalCSSIntoThemeSystem(fs.readFileSync(path.join(hljsPath, 'styles/github-dark.css'), 'utf-8'), 'dark') +} + +const commonCSS = minifyCSS(fs.readFileSync(path.join(scriptDir, 'style.css'), 'utf8')) + +const coomonScript = minifyJS(fs.readFileSync(path.join(scriptDir, 'theme.ts'), 'utf8')) + +const assert = { + ul: (tag: FormattedTag): tag is FormattedTag<'ul'> => { + return tag.tag === 'ul' + }, + pre: (tag: FormattedTag): tag is FormattedTag<'pre' | `pre.${string}`> => { + if (tag.tag.startsWith('pre')) { return true } + return false + }, + base: (tag: FormattedTag): tag is FormattedTag> => { + if (tag.tag !== 'ul') { return true } + return false + } +} + +function toID(text: string) { + return text.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') +} + +function renderMainSection(page: string, pageData: RenderMetadata): string { + const handler = (c: FormattedTag): string => { + if (assert.ul(c)) { + return `
    ${c.value.map((li) => `
  • ${md.renderInline(li.trim())}
  • `).join('')}
` + } + if (assert.pre(c)) { + if (c.tag.startsWith('pre.')) { + const lang = c.tag.split('.')[1] + return `
${hljs.highlight(c.value.trim(), { language: lang }).value}
` + } + return `
${md.render(c.value.trim())}
` + } + if (assert.base(c)) { + // For heading metadata + if (/^h[2-6]$/.test(c.tag)) { + const slug = toID(c.value) + return `<${c.tag} id="${slug}">${ + md.renderInline(c.value.trim()) + }` + } + + return `<${c.tag}>${md.renderInline(c.value.trim())}` + } + throw new Error('Unreachable') + } + return pageData.body + .reduce((acc, cur) => (acc.push(handler(cur)), acc), []) + .join('\n') +} + +interface HeadingBase { + value: string + id: string +} + +interface HeadingMetadata extends HeadingBase { + h3s: HeadingBase[] +} + +interface HeadingStruct { + key: string + title: string + h2s: HeadingMetadata[] +} + +function renderMenu(): string { + const structure: HeadingStruct[] = [] + for (const [pageName, pageData] of formatedPages) { + if (pageName === 'index') { continue } + const h2s: HeadingMetadata[] = [] + const root = { key: pageName, title: pageData.title, h2s } + let h3s: HeadingBase[] = [] + + for (const c of pageData.body) { + if (assert.base(c)) { + if (c.tag === 'h2') { + h3s = [] + h2s.push({ value: c.value, id: toID(c.value), h3s }) + } else if (c.tag === 'h3') { + h3s.push({ value: c.value, id: toID(c.value) }) + } + } + } + structure.push(root) + } + + const navs: string[] = [] + + for (const { key, title, h2s } of structure) { + navs.push(`
  • ${title}
  • `) + for (const h2 of h2s) { + navs.push(`
  • ${h2.value}
  • `) + if (h2.h3s.length > 0) { + navs.push('
      ') + for (const h3 of h2.h3s) { + navs.push(`
    • ${h3.value}
    • `) + } + navs.push('
    ') + } + } + } + return navs.join('') +} + +const icons = { + moon: ` + + + +`.trim(), + sun: ` + + + +`.trim(), + github: ` + + + +`.trim() +} + +function widget() { + const html: string[] = [] + html.push('') + return html +} + +function buildExampleDisplay() { + let html = fs.readFileSync(path.join(devDir, 'index.html'), 'utf8') + html = html.replace(/)<[^<]*)*<\/script>/gi, '') + html = injectHTMLTag({ + html, + injectTo: 'body', + descriptors: { + kind: 'script', + text: buildAndMinifyJS(path.join(devDir, 'main.ts')) + } + }) + return html +} + +async function main() { + for (const [page, pageData] of formatedPages) { + const html: string[] = [] + html.push('') + html.push('') + + // Head + html.push('') + html.push('') + html.push('') + html.push('') + html.push(`squarified - ${pageData.title}`) + html.push('') + html.push('') + html.push('') + html.push('') + html.push(``) + html.push(``) + html.push(``) + html.push('') + + // Body + + html.push('') + + // Menu + + html.push('') + + // Article + html.push('
    ') + html.push(renderMainSection(page, pageData)) + html.push('
    ') + html.push('') + html.push(``) + html.push('') + html.push('\n') + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir) + } + await fsp.writeFile(path.join(destDir, `${page}.html`), html.join(''), 'utf8') + } + const example = buildExampleDisplay() + // cp data.json to display + await fsp.copyFile(path.join(devDir, 'data.json'), path.join(destDir, 'data.json')) + await fsp.writeFile(path.join(destDir, 'example.html'), example, 'utf8') +} + +main().catch(console.error) diff --git a/scripts/serve.ts b/scripts/serve.ts new file mode 100644 index 0000000..8ea7149 --- /dev/null +++ b/scripts/serve.ts @@ -0,0 +1,150 @@ +import chokidar from 'chokidar' +import { EventEmitter } from 'events' +import fs from 'fs' +import http from 'http' +import path from 'path' +import { x } from 'tinyexec' + +const monitorDirs = { + docs: path.join(__dirname, '..', 'docs'), + scripts: __dirname, + lib: path.join(__dirname, '..', 'src') +} + +const MIME_TYPES: Record = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + '.woff': 'application/font-woff', + '.ttf': 'application/font-ttf', + '.eot': 'application/vnd.ms-fontobject', + '.otf': 'application/font-otf', + '.svg': 'application/image/svg+xml' +} + +const watcher = chokidar.watch(Object.values(monitorDirs), { + ignored: (p) => p.includes('serve.ts') +}) + +const liveReloadScript = fs.readFileSync(path.join(__dirname, 'live-reload.js'), 'utf8') + +function createStaticLivingServer() { + const app = http.createServer() + const sse = new SSE() + app.on('request', (req, res) => { + if (req.url === '/nonzzz') { + if (req.headers.accept === 'text/event-stream') { + sse.serverEventStream(req, res) + return + } + } + if (req.url === '/favicon.ico' || req.url === '/favicon.svg') { + res.writeHead(204, { 'Content-Type': 'image/x-icon' }) + res.end() + return + } + let file = req.url === '/' ? 'index.html' : req.url! + + if (!path.extname(file)) { + file += '.html' + } + const filePath = path.join(__dirname, '..', 'display', file) + const ext = path.extname(filePath) + const contentType = MIME_TYPES[ext] || 'text/html' + let content = fs.readFileSync(filePath, 'utf8') + if (ext === '.html') { + content += `` + } + res.writeHead(200, { 'Content-Type': contentType }) + res.write(content) + res.end() + }) + + watcher.on('all', (event, path) => { + if (event === 'change') { + prepareDisplay().then(() => { + sse.sendEvent('change', path) + }).catch(console.error) + } + }) + + return app +} + +async function prepareDisplay() { + const r = await x('./node_modules/.bin/tsx', ['./scripts/render.ts'], { nodeOptions: { cwd: process.cwd() } }) + if (r.stderr) { + throw new Error(r.stderr) + } + return r.exitCode === 0 +} + +async function main() { + await prepareDisplay() + + const server = createStaticLivingServer() + + server.listen(8090, () => { + console.log('Server running on http://localhost:8090') + }) +} + +export interface SSEMessageBody { + event: string + data: string +} + +// This exposes an event stream to clients using server-sent events: +// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events +export class SSE { + private activeStreams: EventEmitter[] = [] + + serverEventStream(req: http.IncomingMessage, res: http.ServerResponse) { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'access-control-allow-origin': '*' + }) + res.write('retry: 500\n') + res.write(':\n\n') + res.flushHeaders() + const stream = new EventEmitter() + this.activeStreams.push(stream) + const keepAliveInterval = setInterval(() => { + res.write(':\n\n') + res.flushHeaders() + }, 3000) + stream.on('message', (msg: SSEMessageBody) => { + res.write(`event: ${msg.event}\ndata: ${msg.data}\n\n`) + res.flushHeaders() + }) + req.on('close', () => { + clearInterval(keepAliveInterval) + this.removeStream(stream) + res.end() + }) + } + + sendEvent(event: string, data: string) { + const message: SSEMessageBody = { event, data } + this.activeStreams.forEach((stream) => { + stream.emit('message', message) + }) + } + + private removeStream(stream: EventEmitter) { + const index = this.activeStreams.indexOf(stream) + if (index !== -1) { + this.activeStreams.splice(index, 1) + } + } +} + +main().catch(console.error) diff --git a/scripts/style.css b/scripts/style.css new file mode 100644 index 0000000..0a7659a --- /dev/null +++ b/scripts/style.css @@ -0,0 +1,243 @@ +html:not([data-theme="dark"]) { + --background-color: #fff; + --foreground-color: #000; + --accents_1: #fafafa; + --theme_display_dark: none; + --theme_display_light: block; + --anchor-color: #000; +} + +:root { + --font_sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font_mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace; +} + +html[data-theme="dark"] { + color-scheme: dark; + --background-color: #000; + --foreground-color: #ddd; + --accents_1: #111; + --theme_display_dark: block; + --theme_display_light: none; + --anchor-color: #ddd; +} + +body { + font-size: 16px; + font-family: var(--font_sans); + line-height: 1.5; +} + +a { + color: inherit; +} + +p, small { + font-weight: 400; + color: inherit; + letter-spacing: -0.005625em; +} + +p { + margin: 1em 0; + font-size: 1em; + line-height: 1.625em; +} + +small { + margin: 0; + font-size: 0.875em; + line-height: 1.5em; +} + +nav a { + text-decoration: none; +} + +b, strong { + color: var(--foreground-color); + font-weight: 600; +} + +ul, ol { + padding: 0; + color: var(--foreground-color); + margin: 8px 8px 8px 16px; + list-style-type: none; +} + +ol { + list-style-type: decimal; +} + +li { + margin-bottom: 0.625em; + font-size: 1em; + line-height: 1.625em; +} + +h1, h2, h3, h4, h5, h6 { + color: inherit; + margin: 0 0 0.7rem 0; +} + +h1 { + font-size: 3rem; + letter-spacing: -0.02em; + line-height: 1.5; + font-weight: 700; +} + +h2 { + font-size: 2.25rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h3 { + font-size: 1.5rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h4 { + font-size: 1.25rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h5 { + font-size: 1rem; + letter-spacing: -0.01em; + font-weight: 600; +} + +h6 { + font-size: 0.875rem; + letter-spacing: -0.005em; + font-weight: 600; +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + margin: 0; +} + +button:focus, +input:focus, +select:focus, +textarea:focus { + outline: none; +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + white-space: pre-wrap; +} + +code:before, +code:after { + content: "`"; +} + +pre { + padding: 14px 16px; + background-color: var(--accents_1); + margin: 16px 0; + font-family: var(--font-mono); + white-space: pre; + line-height: 1.5; + text-align: left; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + font-size: 14px; +} + +#menu-container { + display: none; + position: fixed; + left: -20px; + top: 0; + right: -20px; + height: 50px; + padding: 0 20px; + z-index: 2; +} + +#theme-light { + display: var(--theme_display_light); +} + +#theme-dark { + display: var(--theme_display_dark); +} + +.anchorlink { + opacity: 0; + color: var(--anchor-color); + font-size: 0.85em; + float: left; + text-decoration: none; + margin-left: -1em; + margin-top: 0.15em; +} +.anchorlink:hover { + opacity: 1; +} + +@media screen and (min-width: 800px) { + body { + position: relative; + max-width: 1000px; + margin: 0 auto; + padding: 50px 50px 500px 250px; + } + main { + position: relative; + z-index: 1; + } + #menu { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + max-width: 1300px; + margin: 0 auto; + display: block; + } + #menu > div { + margin-top: 20px; + position: absolute; + left: 0; + top: 0; + bottom: 0; + padding: 50px 0 30px 30px; + } + + #menu #widget { + display: inline-flex; + width: 100%; + justify-content: center; + } + + #menu > div svg { + height: 24px; + width: 24px; + } + + #menu #widget > a:not(:first-child) { + margin-left: 6px; + } + #menu ul li { + cursor: pointer; + } +} diff --git a/scripts/theme.ts b/scripts/theme.ts new file mode 100644 index 0000000..9895848 --- /dev/null +++ b/scripts/theme.ts @@ -0,0 +1,27 @@ +export type Theme = 'light' | 'dark' +;(() => { + const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const doc = document.documentElement + const docDataset = doc.dataset + + const themeButton = document.querySelector('#theme-toggle') + + const preferredDark = darkMediaQuery.matches || localStorage.getItem('theme') === 'dark' + + if (themeButton) { + themeButton.addEventListener('click', toggleTheme) + } + + function updateTeme(theme: Theme) { + localStorage.setItem('theme', theme) + docDataset.theme = theme + console.log(`Theme set to ${theme}`, docDataset) + } + + function toggleTheme() { + const theme = docDataset.theme === 'light' ? 'dark' : 'light' + updateTeme(theme) + } + + updateTeme(preferredDark ? 'dark' : 'light') +})() diff --git a/src/primitives/struct.ts b/src/primitives/struct.ts index 7c03078..b4b33ad 100644 --- a/src/primitives/struct.ts +++ b/src/primitives/struct.ts @@ -3,8 +3,6 @@ import { perferNumeric } from '../shared' import type { LayoutModule } from './squarify' -type AnyObject = Record - export function sortChildrenByKey(data: T[], ...keys: K[]) { return data.sort((a, b) => { for (const key of keys) {