More customzation

This commit is contained in:
2025-11-28 15:29:39 +08:00
parent 985164f4c5
commit 9133d23a15
85 changed files with 4176 additions and 1439 deletions

254
bun.lock
View File

@@ -21,7 +21,9 @@
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fslightbox": "^3.7.4",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"mermaid": "^11.12.1",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"radix-ui": "^1.3.4", "radix-ui": "^1.3.4",
"react": "19.0.0", "react": "19.0.0",
@@ -110,8 +112,20 @@
"@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], "@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="],
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="],
"@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="],
"@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="],
"@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="], "@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
@@ -260,6 +274,8 @@
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -488,6 +504,68 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
@@ -496,6 +574,8 @@
"@types/fontkit": ["@types/fontkit@2.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="], "@types/fontkit": ["@types/fontkit@2.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
@@ -518,6 +598,8 @@
"@types/tar": ["@types/tar@6.1.13", "", { "dependencies": { "@types/node": "*", "minipass": "^4.0.0" } }, "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw=="], "@types/tar": ["@types/tar@6.1.13", "", { "dependencies": { "@types/node": "*", "minipass": "^4.0.0" } }, "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
@@ -638,6 +720,10 @@
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@@ -682,6 +768,8 @@
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -702,6 +790,80 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="],
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
"dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "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=="], "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
@@ -710,6 +872,8 @@
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -740,6 +904,8 @@
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
@@ -846,6 +1012,8 @@
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fslightbox": ["fslightbox@3.7.4", "", {}, "sha512-zQqMHxiYkR0W/xrWQlchoO626C5KCM6rabpMWiJsy+MZCMHo7zlywsGAOGeOahRUqBZzXT9OeMddiVSfW77gaA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
@@ -876,6 +1044,8 @@
"h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="],
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
@@ -938,6 +1108,8 @@
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-absolute-url": ["is-absolute-url@4.0.1", "", {}, "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A=="], "is-absolute-url": ["is-absolute-url@4.0.1", "", {}, "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A=="],
@@ -994,12 +1166,18 @@
"katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": "cli.js" }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="], "katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": "cli.js" }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="],
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="],
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
@@ -1026,6 +1204,8 @@
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -1040,6 +1220,8 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
@@ -1082,6 +1264,8 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"mermaid": ["mermaid@11.12.1", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g=="],
"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": ["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-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=="],
@@ -1240,6 +1424,8 @@
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -1254,6 +1440,10 @@
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
@@ -1368,10 +1558,16 @@
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rollup": ["rollup@4.41.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.0", "@rollup/rollup-android-arm64": "4.41.0", "@rollup/rollup-darwin-arm64": "4.41.0", "@rollup/rollup-darwin-x64": "4.41.0", "@rollup/rollup-freebsd-arm64": "4.41.0", "@rollup/rollup-freebsd-x64": "4.41.0", "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", "@rollup/rollup-linux-arm-musleabihf": "4.41.0", "@rollup/rollup-linux-arm64-gnu": "4.41.0", "@rollup/rollup-linux-arm64-musl": "4.41.0", "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-musl": "4.41.0", "@rollup/rollup-linux-s390x-gnu": "4.41.0", "@rollup/rollup-linux-x64-gnu": "4.41.0", "@rollup/rollup-linux-x64-musl": "4.41.0", "@rollup/rollup-win32-arm64-msvc": "4.41.0", "@rollup/rollup-win32-ia32-msvc": "4.41.0", "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg=="], "rollup": ["rollup@4.41.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.0", "@rollup/rollup-android-arm64": "4.41.0", "@rollup/rollup-darwin-arm64": "4.41.0", "@rollup/rollup-darwin-x64": "4.41.0", "@rollup/rollup-freebsd-arm64": "4.41.0", "@rollup/rollup-freebsd-x64": "4.41.0", "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", "@rollup/rollup-linux-arm-musleabihf": "4.41.0", "@rollup/rollup-linux-arm64-gnu": "4.41.0", "@rollup/rollup-linux-arm64-musl": "4.41.0", "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-musl": "4.41.0", "@rollup/rollup-linux-s390x-gnu": "4.41.0", "@rollup/rollup-linux-x64-gnu": "4.41.0", "@rollup/rollup-linux-x64-musl": "4.41.0", "@rollup/rollup-win32-arm64-msvc": "4.41.0", "@rollup/rollup-win32-ia32-msvc": "4.41.0", "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg=="],
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="], "s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -1426,6 +1622,8 @@
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="], "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
"suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="], "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -1456,6 +1654,8 @@
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "bin": "bin/tsconfck.js" }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "bin": "bin/tsconfck.js" }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1522,6 +1722,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "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-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
@@ -1632,26 +1834,28 @@
"@iconify/utils/local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="], "@iconify/utils/local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
"@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@types/tar/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "@types/tar/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"boxen/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "boxen/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -1660,9 +1864,11 @@
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "langium/vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
"minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "mermaid/@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -1674,26 +1880,16 @@
"patch-package/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "patch-package/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"patch-package/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"sharp/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"svgo/css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], "svgo/css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
"typescript-auto-import-cache/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"volar-service-typescript/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1740,23 +1936,21 @@
"@iconify/utils/local-pkg/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="], "@iconify/utils/local-pkg/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "mermaid/@iconify/utils/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"svgo/css-tree/mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], "svgo/css-tree/mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
@@ -1778,10 +1972,12 @@
"@iconify/utils/local-pkg/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@iconify/utils/local-pkg/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"mermaid/@iconify/utils/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"mermaid/@iconify/utils/mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="], "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="],

View File

@@ -30,7 +30,9 @@
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fslightbox": "^3.7.4",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"mermaid": "^11.12.1",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"radix-ui": "^1.3.4", "radix-ui": "^1.3.4",
"react": "19.0.0", "react": "19.0.0",

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
public/favicon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

1
public/js/fslightbox.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" /> <link rel="icon" type="image/png" href="/favicon.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <!-- <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> -->
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.webp" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <!-- <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> -->
<meta name="apple-mobile-web-app-title" content="astro-erudite" /> <!-- <meta name="apple-mobile-web-app-title" content="astro-erudite" /> -->
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />

View File

@@ -3,7 +3,7 @@ import Link from '@/components/Link.astro'
import ThemeToggle from '@/components/ThemeToggle.astro' import ThemeToggle from '@/components/ThemeToggle.astro'
import { NAV_LINKS, SITE } from '@/consts' import { NAV_LINKS, SITE } from '@/consts'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import logo from '../../public/static/logo.svg' import logo from '../../public/favicon.png'
--- ---
<div> <div>

View File

@@ -0,0 +1,105 @@
---
import BulletedListItems from './notion/BulletedListItems.astro'
import Callout from './notion/Callout.astro'
import Code from './notion/Code.astro'
import ColumnList from './notion/ColumnList.astro'
import Divider from './notion/Divider.astro'
import Bookmark from './notion/Bookmark.astro'
import Embed from './notion/Embed.astro'
import File from './notion/File.astro'
import Heading1 from './notion/Heading1.astro'
import Heading2 from './notion/Heading2.astro'
import Heading3 from './notion/Heading3.astro'
import Image from './notion/Image.astro'
import LinkToPage from './notion/LinkToPage.astro'
import NumberedListItems from './notion/NumberedListItems.astro'
import Paragraph from './notion/Paragraph.astro'
import Quote from './notion/Quote.astro'
import SyncedBlock from './notion/SyncedBlock.astro'
import Table from './notion/Table.astro'
import TableOfContents from './notion/TableOfContents.astro'
import ToDo from './notion/ToDo.astro'
import Toggle from './notion/Toggle.astro'
import Video from './notion/Video.astro'
import type * as interfaces from '../lib/interfaces.ts'
export interface Props {
blocks: interfaces.Block[]
level?: number
urlMap?: { [key: string]: string }
headings?: interfaces.Block[]
}
const {
blocks = [],
level = 0,
urlMap = {},
headings = [],
}: Props = Astro.props
---
{
blocks.map((block) => {
switch (block.Type) {
case 'paragraph':
return <Paragraph block={block} headings={headings} />
case 'heading_1':
return <Heading1 block={block} headings={headings} />
case 'heading_2':
return <Heading2 block={block} headings={headings} />
case 'heading_3':
return <Heading3 block={block} headings={headings} />
case 'bulleted_list':
return (
<BulletedListItems
block={block}
level={level}
headings={headings}
/>
)
case 'numbered_list':
return (
<NumberedListItems
block={block}
level={level}
headings={headings}
/>
)
case 'to_do':
return <ToDo block={block} headings={headings} />
case 'code':
return <Code block={block} />
case 'quote':
return <Quote block={block} headings={headings} />
case 'callout':
return <Callout block={block} headings={headings} />
case 'image':
return <Image block={block} />
case 'file':
return <File block={block} />
case 'embed':
return <Embed block={block} urlMap={urlMap} />
case 'bookmark':
case 'link_preview':
return <Bookmark block={block} urlMap={urlMap} />
case 'video':
return <Video block={block} />
case 'divider':
return <Divider />
case 'table':
return <Table block={block} />
case 'column_list':
return <ColumnList block={block} headings={headings} />
case 'synced_block':
return <SyncedBlock block={block} headings={headings} />
case 'toggle':
return <Toggle block={block} headings={headings} />
case 'table_of_contents':
return <TableOfContents block={block} headings={headings} />
case 'link_to_page':
return <LinkToPage block={block} />
default:
return null
}
})
}

View File

@@ -11,11 +11,18 @@ const { post } = Astro.props
const title = post.data.title || SITE.title const title = post.data.title || SITE.title
const description = post.data.description || SITE.description const description = post.data.description || SITE.description
const image = new URL('/static/1200x630.png', Astro.site) const fallbackOgImage = new URL('/static/1200x630.png', Astro.site).toString()
const author = const author =
post.data.authors && post.data.authors.length > 0 post.data.authors && post.data.authors.length > 0
? post.data.authors.join(', ') ? post.data.authors.join(', ')
: SITE.author : SITE.author
const heroImage = post.data.banner ?? post.data.image
const heroImageUrl =
typeof heroImage === 'string'
? heroImage
: heroImage?.src
? `${SITE.href}${heroImage.src}`
: fallbackOgImage
--- ---
<title>{`${title} | ${SITE.title}`}</title> <title>{`${title} | ${SITE.title}`}</title>
@@ -27,12 +34,7 @@ const author =
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta <meta property="og:image" content={heroImageUrl} />
property="og:image"
content={post?.data?.image?.src
? `${SITE.href}${post.data.image.src}`
: image}
/>
<meta property="og:image:alt" content={title} /> <meta property="og:image:alt" content={title} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content={SITE.locale} /> <meta property="og:locale" content={SITE.locale} />
@@ -42,12 +44,7 @@ const author =
<meta name="twitter:title" content={title} /> <meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} /> <meta name="twitter:description" content={description} />
<meta <meta property="twitter:image" content={heroImageUrl} />
property="twitter:image"
content={post?.data?.image?.src
? `${SITE.href}${post.data.image.src}`
: image}
/>
<meta name="twitter:image:alt" content={title} /> <meta name="twitter:image:alt" content={title} />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content={author} /> <meta name="twitter:creator" content={author} />

View File

@@ -31,42 +31,134 @@ import { Icon } from 'astro-icon/components'
</script> </script>
<script> <script>
function handleToggleClick() { const VIEW_TRANSITION_STYLE_ID = 'view-transition-style'
const VIEW_TRANSITION_STYLE = `
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
[data-theme='dark']::view-transition-old(root) {
z-index: 1;
}
[data-theme='dark']::view-transition-new(root) {
z-index: 999;
}
::view-transition-old(root) {
z-index: 999;
}
::view-transition-new(root) {
z-index: 1;
}
`
const setTheme = (newTheme) => {
const element = document.documentElement const element = document.documentElement
const currentTheme = element.getAttribute('data-theme')
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
element.classList.add('[&_*]:transition-none') element.classList.add('[&_*]:transition-none')
element.setAttribute('data-theme', newTheme) element.setAttribute('data-theme', newTheme)
localStorage.setItem('theme', newTheme)
window.getComputedStyle(element).getPropertyValue('opacity') window.getComputedStyle(element).getPropertyValue('opacity')
requestAnimationFrame(() => { requestAnimationFrame(() => {
element.classList.remove('[&_*]:transition-none') element.classList.remove('[&_*]:transition-none')
}) })
localStorage.setItem('theme', newTheme) ensureViewTransitionStyle()
}
const ensureViewTransitionStyle = () => {
if (!('startViewTransition' in document)) return
if (document.getElementById(VIEW_TRANSITION_STYLE_ID)) return
const styleElement = document.createElement('style')
styleElement.id = VIEW_TRANSITION_STYLE_ID
styleElement.textContent = VIEW_TRANSITION_STYLE
document.head.appendChild(styleElement)
}
const handleToggleClick = (event) => {
const element = document.documentElement
const currentTheme = element.getAttribute('data-theme') === 'dark'
? 'dark'
: 'light'
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches
if (prefersReducedMotion) {
setTheme(newTheme)
return
}
if (!document.startViewTransition || !event) {
setTheme(newTheme)
return
}
const x = event.clientX ?? window.innerWidth / 2
const y = event.clientY ?? window.innerHeight / 2
const endRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y),
)
try {
document
.startViewTransition(() => {
setTheme(newTheme)
})
.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath:
currentTheme === 'dark' ? [...clipPath].reverse() : clipPath,
},
{
duration: 400,
easing: 'ease-in',
fill: 'both',
pseudoElement:
currentTheme === 'dark'
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
)
})
.catch((error) => {
console.error('Theme transition failed:', error)
setTheme(newTheme)
})
} catch (error) {
console.error('Failed to start view transition:', error)
setTheme(newTheme)
}
} }
function initThemeToggle() { function initThemeToggle() {
document const toggle = document.getElementById('theme-toggle')
.getElementById('theme-toggle') if (!toggle) return
?.addEventListener('click', handleToggleClick)
toggle.removeEventListener('click', handleToggleClick)
toggle.addEventListener('click', handleToggleClick)
} }
initThemeToggle() initThemeToggle()
document.addEventListener('astro:after-swap', () => { document.addEventListener('astro:after-swap', () => {
const storedTheme = localStorage.getItem('theme') || 'light' const storedTheme = localStorage.getItem('theme') || 'light'
const element = document.documentElement setTheme(storedTheme)
element.classList.add('[&_*]:transition-none')
window.getComputedStyle(element).getPropertyValue('opacity')
element.setAttribute('data-theme', storedTheme)
requestAnimationFrame(() => {
element.classList.remove('[&_*]:transition-none')
})
initThemeToggle() initThemeToggle()
}) })
</script> </script>

View File

@@ -0,0 +1,150 @@
---
import type * as interfaces from '../../lib/interfaces.ts'
import { isAmazonURL, isGitHubURL } from '../../lib/blog-helpers.ts'
import GithubLinkPreview from './GitHubLinkPreview.astro'
import Caption from './Caption.astro'
export interface Props {
block: interfaces.Block
urlMap: { [key: string]: string }
}
const { block } = Astro.props
const target = block.Bookmark || block.LinkPreview || block.Embed
const urlString = target?.Url
let url: URL | null = null
try {
if (urlString) {
url = new URL(urlString)
}
} catch (err) {
console.log(err)
}
---
{
url && (
<>
{block.LinkPreview && isGitHubURL(url) ? (
<GithubLinkPreview url={url} />
) : isAmazonURL(url) ? (
<div class="no-metadata">
<a href={url.toString()}>{url.toString()}</a>
</div>
) : (
<div class="bookmark">
<a href={url.toString()} target="_blank" rel="noopener noreferrer">
<div>
<div>{target?.Url}</div>
<div class="muted">{url.hostname}</div>
<div>
<div>
<img
src={`https://www.google.com/s2/favicons?domain=${url.hostname}`}
alt="Favicon of the bookmark site"
loading="lazy"
/>
</div>
<div>{url.origin}</div>
</div>
</div>
</a>
<Caption richTexts={block.Bookmark?.Caption ?? []} />
</div>
)}
</>
)
}
<style>
.no-metadata > a {
border-bottom: 0.05em solid;
border-color: var(--anchor-border);
opacity: 0.7;
}
.bookmark {
display: block;
overflow: hidden;
width: 100%;
max-width: 100%;
font-size: 0.9rem;
}
.bookmark > a {
width: 100%;
box-sizing: border-box;
text-decoration: none;
border: 1px solid rgba(55, 53, 47, 0.16);
border-radius: 3px;
display: flex;
overflow: hidden;
user-select: none;
}
.bookmark > a > div:first-child {
flex: 4 1 180px;
padding: 12px 14px 14px;
overflow: hidden;
text-align: left;
color: var(--fg);
}
.bookmark > a > div:first-child > div:first-child {
width: 120px;
min-width: 100%;
font-size: 14px;
line-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 24px;
margin-bottom: 2px;
}
.bookmark > a > div:first-child > div:nth-child(2) {
font-size: 12px;
line-height: 16px;
opacity: 0.8;
height: 32px;
overflow: hidden;
}
.bookmark > a > div:first-child > div:last-child {
display: flex;
margin-top: 6px;
}
.bookmark > a > div:first-child > div:last-child > div:first-child {
width: 16px;
height: 16px;
min-width: 16px;
margin-right: 6px;
}
.bookmark > a > div:first-child > div:last-child > div:first-child > img {
max-width: 100%;
display: inline-block;
}
.bookmark > a > div:first-child > div:last-child > div:last-child {
font-size: 12px;
line-height: 16px;
color: var(--fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark > a > div:last-child {
flex: 1 1 180px;
position: relative;
}
@media (max-width: 640px) {
.bookmark > a > div:last-child {
display: none;
}
}
.bookmark > a > div:last-child > img {
position: absolute !important;
width: 100%;
height: 100%;
object-fit: cover;
}
.bookmark .muted {
font-size: 12px;
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,51 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
level?: number
headings: interfaces.Block[]
}
const { block, level = 0, headings }: Props = Astro.props
const items = block.ListItems ?? []
const listTypes = ['disc', 'circle', 'square']
---
<ul class={`list-${listTypes[level % listTypes.length]}`}>
{
items
.filter(
(b: interfaces.Block) => b.Type === 'bulleted_list_item' && b.BulletedListItem,
)
.map((b: interfaces.Block) => {
const item = b.BulletedListItem
if (!item) return null
return (
<li class={snakeToKebab(item.Color)}>
{item.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
{item.Children && item.Children.length > 0 && (
<NotionBlocks
blocks={item.Children}
level={level + 1}
headings={headings}
/>
)}
</li>
)
})
}
</ul>
<style>
ul {
font-size: 1rem;
}
</style>

View File

@@ -0,0 +1,73 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Callout) {
return null
}
const callout = block.Callout
---
<div class={`callout ${snakeToKebab(callout.Color)}`}>
{
callout.Icon && (
<div class="icon">
{callout.Icon.Type === 'emoji' ? (
callout.Icon.Emoji
) : callout.Icon.Type === 'external' ? (
<img src={callout.Icon.Url} alt="Icon in a callout block" />
) : null}
</div>
)
}
<div>
{
callout.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))
}
{
callout.Children && (
<NotionBlocks blocks={callout.Children} headings={headings} />
)
}
</div>
</div>
<style>
.callout {
display: flex;
margin: 0.4rem auto;
padding: 16px 12px;
width: 100%;
font-size: 1rem;
font-weight: 400;
line-height: 1.6rem;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
background: rgba(235, 236, 237, 0.6);
}
.callout > div {
margin: 0;
line-height: 1.5rem;
}
.callout > div.icon {
margin-right: 0.7rem;
}
.callout > div.icon > img {
width: 1.2rem;
height: 1.2rem;
}
</style>

View File

@@ -0,0 +1,33 @@
---
import type { RichText } from '../../lib/interfaces.ts'
export interface Props {
richTexts: RichText[]
}
const { richTexts } = Astro.props
---
{
richTexts.length > 0 && richTexts[0]?.Text?.Content && (
<div class="caption">
<div>{richTexts[0]?.Text?.Content}</div>
</div>
)
}
<style>
.caption {
display: flex;
margin-top: 0.3rem;
font-size: 0.9rem;
color: var(--accents-3);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.4;
}
.caption > div {
flex-grow: 1;
width: 0;
}
</style>

View File

@@ -0,0 +1,26 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
const param = url.searchParams.get('ctz')
---
<div class="circuit-simulator-applet-wrapper">
<iframe
src={`https://www.falstad.com/circuit/circuitjs.html?ctz=${param}`}
allowfullscreen></iframe>
</div>
<style>
.circuit-simulator-applet-wrapper {
margin: 0.4rem auto;
width: 100%;
aspect-ratio: 4 / 3;
}
.circuit-simulator-applet-wrapper iframe {
width: 100%;
height: 100%;
border: 1px solid var(--fg);
}
</style>

View File

@@ -0,0 +1,132 @@
---
import Prism from 'prismjs'
import 'prismjs/components/prism-css'
import 'prismjs/components/prism-diff'
import 'prismjs/components/prism-docker'
import 'prismjs/components/prism-elixir'
import 'prismjs/components/prism-go'
import 'prismjs/components/prism-hcl'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-ruby'
import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-typescript'
import 'prismjs/components/prism-yaml'
import * as interfaces from '../../lib/interfaces'
import '../../styles/syntax-coloring.css'
import Caption from './Caption.astro'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.Code) {
return null
}
const code = block.Code.RichTexts.map(
(richText: interfaces.RichText) => richText.Text?.Content || '',
).join('')
const language = block.Code.Language.toLowerCase()
const grammer =
Prism.languages[language.toLowerCase()] || Prism.languages.javascript
---
<div class="code">
<div>
{
/* prettier-ignore */
language === 'mermaid' ? (
<pre class="mermaid">{code}</pre>
) : (
<>
<div>
<button class="copy" data-code={code} data-done-text="Copied!">
Copy
</button>
</div>
<pre><code set:html={Prism.highlight(code, grammer, language)} /></pre>
</>
)
}
</div>
<Caption richTexts={block.Code.Caption} />
</div>
<script>
document.querySelectorAll('button.copy').forEach((button) => {
button.addEventListener('click', (ev) => {
const target = ev.target as HTMLElement | null
if (!target) return
navigator.clipboard
.writeText(target.getAttribute('data-code') || '')
.then(() => {
const originalText = target.innerText
target.innerText = target.getAttribute('data-done-text') || target.innerText
setTimeout(() => {
target.innerText = originalText
}, 3000)
})
})
})
</script>
<style>
.code {
display: block;
width: 100%;
margin-bottom: 0.6rem;
}
.code > div {
background: rgb(247, 246, 243);
border-radius: var(--radius);
}
.code > div div {
display: flex;
justify-content: flex-end;
}
.code button.copy {
display: block;
width: 4rem;
border: 0;
border-radius: var(--radius);
background-color: rgba(227, 226, 224, 0.5);
color: var(--fg);
line-height: 1.2rem;
cursor: pointer;
}
.code pre {
display: block;
overflow: auto;
padding: 0.8rem 2rem 2rem;
font-size: 0.9rem;
line-height: 1.2rem;
white-space: pre;
width: 100px;
min-width: 100%;
overflow-x: auto;
&::-webkit-scrollbar {
height: 10px;
}
&::-webkit-scrollbar-thumb {
background: rgb(211, 209, 203);
}
&::-webkit-scrollbar-track {
background: rgb(237, 236, 233);
}
}
.code pre.mermaid {
padding: 2rem;
}
.code pre code {
color: var(--fg);
padding: 0;
background: rgb(247, 246, 243) !important;
border-radius: 0;
}
</style>

View File

@@ -0,0 +1,28 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
const user = url.pathname.split('/')[1]
const id = url.pathname.split('/')[3]
---
<p
class="codepen"
data-slug-hash={id.toString()}
data-user={user.toString()}
style="box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"
>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<style is:global>
.cp_embed_wrapper {
width: 100%;
aspect-ratio: 1.6 / 1;
background-color: #fff;
}
.cp_embed_wrapper iframe {
height: 100% !important;
}
</style>

View File

@@ -0,0 +1,46 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import NotionBlocks from '../NotionBlocks.astro'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.ColumnList) {
return null
}
---
<div class="column-list">
{
block.ColumnList.Columns.map((column: interfaces.Column) => (
<div>
<NotionBlocks blocks={column.Children} headings={headings} />
</div>
))
}
</div>
<style>
.column-list {
display: flex;
width: 100%;
margin: 1rem auto;
gap: 0 1rem;
}
.column-list > div {
flex: 1 1 180px;
width: 180px;
}
@media (max-width: 640px) {
.column-list {
display: block;
}
.column-list > div {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,11 @@
---
---
<hr class="divider" />
<style>
.divider {
margin: 1rem 0;
background-color: #dedede;
}
</style>

View File

@@ -0,0 +1,57 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import {
isTweetURL,
isTikTokURL,
isInstagramURL,
isPinterestURL,
isCodePenURL,
isCircuitSimulatorAppletURL,
} from '../../lib/blog-helpers.ts'
import Bookmark from './Bookmark.astro'
import TweetEmbed from './TweetEmbed.astro'
import TikTokEmbed from './TikTokEmbed.astro'
import InstagramEmbed from './InstagramEmbed.astro'
import PinterestEmbed from './PinterestEmbed.astro'
import CodePenEmbed from './CodePenEmbed.astro'
import CircuitSimulatorAppletEmbed from './CircuitSimulatorAppletEmbed.astro'
export interface Props {
block: interfaces.Block
urlMap: { [key: string]: string }
}
const { block, urlMap }: Props = Astro.props
if (!block.Embed) {
return null
}
let url: URL
try {
url = new URL(block.Embed.Url)
} catch (err) {
console.log(err)
url = null
}
---
{
url ? (
isTweetURL(url) ? (
<TweetEmbed url={url} />
) : isTikTokURL(url) ? (
<TikTokEmbed url={url} />
) : isInstagramURL(url) ? (
<InstagramEmbed url={url} />
) : isPinterestURL(url) ? (
<PinterestEmbed url={url} />
) : isCodePenURL(url) ? (
<CodePenEmbed url={url} />
) : isCircuitSimulatorAppletURL(url) ? (
<CircuitSimulatorAppletEmbed url={url} />
) : (
<Bookmark block={block} urlMap={urlMap} />
)
) : null
}

View File

@@ -0,0 +1,27 @@
---
import katex from 'katex'
import * as interfaces from '../../lib/interfaces.ts'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.Equation) {
return null
}
---
<div
class="equation"
set:html={katex.renderToString(block.Equation.Expression, {
throwOnError: false,
})}
/>
<style>
.equation {
text-align: center;
}
</style>

View File

@@ -0,0 +1,75 @@
---
import * as interfaces from '../../lib/interfaces'
import { filePath } from '../../lib/blog-helpers'
import Caption from './Caption.astro'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.File) {
return null
}
let url: URL
try {
const source = block.File.External?.Url || block.File.File?.Url
if (!source) {
throw new Error('Invalid file URL')
}
url = new URL(source)
} catch (err) {
console.error(`Invalid file URL. error: ${err}`)
url = null
}
const filename =
url &&
decodeURIComponent(url.pathname.split('/').slice(-1)[0] || '')
---
<div class="file">
<div>
{
url && (
<a
href={block.File.External ? url.toString() : filePath(url)}
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://www.notion.so/icons/document_gray.svg"
alt="File icon in a file block"
/>{' '}
{filename}
</a>
)
}
</div>
<Caption richTexts={block.File.Caption} />
</div>
<style>
.file {
}
.file a {
display: block;
padding: 0.5rem 0.2rem 0.4rem;
border-radius: var(--radius);
color: var(--fg);
font-weight: 500;
line-height: 1.4rem;
}
.file a:hover {
background-color: #eee;
}
.file a img {
width: 1.3rem;
height: 1.3rem;
vertical-align: sub;
}
</style>

View File

@@ -0,0 +1,315 @@
---
export interface Props {
url: URL
}
type EmbedStyle =
| 'default'
| 'a11y-dark'
| 'a11y-light'
| 'agate'
| 'an-old-hope'
| 'androidstudio'
| 'arduino-light'
| 'arta'
| 'ascetic'
| 'atom-one-dark'
| 'atom-one-dark-reasonable'
| 'atom-one-light'
| 'brown-paper'
| 'codepen-embed'
| 'color-brewer'
| 'dark'
| 'devibeans'
| 'docco'
| 'far'
| 'felipec'
| 'foundation'
| 'github'
| 'github-dark'
| 'github-dark-dimmed'
| 'gml'
| 'googlecode'
| 'gradient-dark'
| 'gradient-light'
| 'grayscale'
| 'hybrid'
| 'idea'
| 'intellij-light'
| 'ir-black'
| 'isbl-editor-dark'
| 'isbl-editor-light'
| 'kimbie-dark'
| 'kimbie-light'
| 'lightfair'
| 'lioshi'
| 'magula'
| 'mono-blue'
| 'monokai'
| 'monokai-sublime'
| 'night-owl'
| 'nnfx-dark'
| 'nnfx-light'
| 'nord'
| 'obsidian'
| 'panda-syntax-dark'
| 'panda-syntax-light'
| 'paraiso-dark'
| 'paraiso-light'
| 'pojoaque'
| 'purebasic'
| 'qtcreator-dark'
| 'qtcreator-light'
| 'rainbow'
| 'routeros'
| 'school-book'
| 'shades-of-purple'
| 'srcery'
| 'stackoverflow-dark'
| 'stackoverflow-light'
| 'sunburst'
| 'tokyo-night-dark'
| 'tokyo-night-light'
| 'tomorrow-night-blue'
| 'tomorrow-night-bright'
| 'vs'
| 'vs2015'
| 'xcode'
| 'xt256'
| 'base16/3024'
| 'base16/apathy'
| 'base16/apprentice'
| 'base16/ashes'
| 'base16/atelier-cave'
| 'base16/atelier-cave-light'
| 'base16/atelier-dune'
| 'base16/atelier-dune-light'
| 'base16/atelier-estuary'
| 'base16/atelier-estuary-light'
| 'base16/atelier-forest'
| 'base16/atelier-forest-light'
| 'base16/atelier-heath'
| 'base16/atelier-heath-light'
| 'base16/atelier-lakeside'
| 'base16/atelier-lakeside-light'
| 'base16/atelier-plateau'
| 'base16/atelier-plateau-light'
| 'base16/atelier-savanna'
| 'base16/atelier-savanna-light'
| 'base16/atelier-seaside'
| 'base16/atelier-seaside-light'
| 'base16/atelier-sulphurpool'
| 'base16/atelier-sulphurpool-light'
| 'base16/atlas'
| 'base16/bespin'
| 'base16/black-metal'
| 'base16/black-metal-bathory'
| 'base16/black-metal-burzum'
| 'base16/black-metal-dark-funeral'
| 'base16/black-metal-gorgoroth'
| 'base16/black-metal-immortal'
| 'base16/black-metal-khold'
| 'base16/black-metal-marduk'
| 'base16/black-metal-mayhem'
| 'base16/black-metal-nile'
| 'base16/black-metal-venom'
| 'base16/brewer'
| 'base16/bright'
| 'base16/brogrammer'
| 'base16/brush-trees'
| 'base16/brush-trees-dark'
| 'base16/chalk'
| 'base16/circus'
| 'base16/classic-dark'
| 'base16/classic-light'
| 'base16/codeschool'
| 'base16/colors'
| 'base16/cupcake'
| 'base16/cupertino'
| 'base16/danqing'
| 'base16/darcula'
| 'base16/dark-violet'
| 'base16/darkmoss'
| 'base16/darktooth'
| 'base16/decaf'
| 'base16/default-dark'
| 'base16/default-light'
| 'base16/dirtysea'
| 'base16/dracula'
| 'base16/edge-dark'
| 'base16/edge-light'
| 'base16/eighties'
| 'base16/embers'
| 'base16/equilibrium-dark'
| 'base16/equilibrium-gray-dark'
| 'base16/equilibrium-gray-light'
| 'base16/equilibrium-light'
| 'base16/espresso'
| 'base16/eva'
| 'base16/eva-dim'
| 'base16/flat'
| 'base16/framer'
| 'base16/fruit-soda'
| 'base16/gigavolt'
| 'base16/github'
| 'base16/google-dark'
| 'base16/google-light'
| 'base16/grayscale-dark'
| 'base16/grayscale-light'
| 'base16/green-screen'
| 'base16/gruvbox-dark-hard'
| 'base16/gruvbox-dark-medium'
| 'base16/gruvbox-dark-pale'
| 'base16/gruvbox-dark-soft'
| 'base16/gruvbox-light-hard'
| 'base16/gruvbox-light-medium'
| 'base16/gruvbox-light-soft'
| 'base16/hardcore'
| 'base16/harmonic16-dark'
| 'base16/harmonic16-light'
| 'base16/heetch-dark'
| 'base16/heetch-light'
| 'base16/helios'
| 'base16/hopscotch'
| 'base16/horizon-dark'
| 'base16/horizon-light'
| 'base16/humanoid-dark'
| 'base16/humanoid-light'
| 'base16/ia-dark'
| 'base16/ia-light'
| 'base16/icy-dark'
| 'base16/ir-black'
| 'base16/isotope'
| 'base16/kimber'
| 'base16/london-tube'
| 'base16/macintosh'
| 'base16/marrakesh'
| 'base16/materia'
| 'base16/material'
| 'base16/material-darker'
| 'base16/material-lighter'
| 'base16/material-palenight'
| 'base16/material-vivid'
| 'base16/mellow-purple'
| 'base16/mexico-light'
| 'base16/mocha'
| 'base16/monokai'
| 'base16/nebula'
| 'base16/nord'
| 'base16/nova'
| 'base16/ocean'
| 'base16/oceanicnext'
| 'base16/one-light'
| 'base16/onedark'
| 'base16/outrun-dark'
| 'base16/papercolor-dark'
| 'base16/papercolor-light'
| 'base16/paraiso'
| 'base16/pasque'
| 'base16/phd'
| 'base16/pico'
| 'base16/pop'
| 'base16/porple'
| 'base16/qualia'
| 'base16/railscasts'
| 'base16/rebecca'
| 'base16/ros-pine'
| 'base16/ros-pine-dawn'
| 'base16/ros-pine-moon'
| 'base16/sagelight'
| 'base16/sandcastle'
| 'base16/seti-ui'
| 'base16/shapeshifter'
| 'base16/silk-dark'
| 'base16/silk-light'
| 'base16/snazzy'
| 'base16/solar-flare'
| 'base16/solar-flare-light'
| 'base16/solarized-dark'
| 'base16/solarized-light'
| 'base16/spacemacs'
| 'base16/summercamp'
| 'base16/summerfruit-dark'
| 'base16/summerfruit-light'
| 'base16/synth-midnight-terminal-dark'
| 'base16/synth-midnight-terminal-light'
| 'base16/tango'
| 'base16/tender'
| 'base16/tomorrow'
| 'base16/tomorrow-night'
| 'base16/twilight'
| 'base16/unikitty-dark'
| 'base16/unikitty-light'
| 'base16/vulcan'
| 'base16/windows-10'
| 'base16/windows-10-light'
| 'base16/windows-95'
| 'base16/windows-95-light'
| 'base16/windows-high-contrast'
| 'base16/windows-high-contrast-light'
| 'base16/windows-nt'
| 'base16/windows-nt-light'
| 'base16/woodland'
| 'base16/xcode-dusk'
| 'base16/zenburn'
type EmbedParams = {
style: EmbedStyle
type: 'code' | 'markdown' | 'ipynb'
showBorder?: 'on'
showLineNumbers?: 'on'
showFileMeta?: 'on'
showFullPath?: 'on'
showCopy?: 'on'
fetchFromJsDelivr?: 'on'
maxHeight?: number
}
function buildEmbedQuery(params: EmbedParams) {
return Object.entries(params)
.filter(([, v]) => v !== undefined)
.map(
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`
)
.join('&')
}
/* Edit Params */
const EMBED_PARAMS: EmbedParams = {
style: 'github',
type: 'code',
showBorder: 'on',
showLineNumbers: 'on',
showFileMeta: 'on',
showFullPath: 'on',
showCopy: 'on',
}
const { url } = Astro.props
const PreviewURL = encodeURIComponent(url.toString())
const embedQuery = buildEmbedQuery(EMBED_PARAMS)
const embedScriptSrc = `https://emgithub.com/embed-v2.js?target=${PreviewURL}&${embedQuery}`
---
<div class="github-link-preview-wrapper">
<div class="github-link-preview">
<script is:inline src={embedScriptSrc}></script>
</div>
</div>
<style>
.github-link-preview-wrapper {
display: flex;
}
.github-link-preview {
flex: 1;
width: 0;
table {
white-space: unset;
}
td::after {
display: unset;
}
}
</style>

View File

@@ -0,0 +1,86 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { buildHeadingId } from '../../lib/blog-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Heading1) {
return null
}
const id = buildHeadingId(block.Heading1)
---
{
block.Heading1.IsToggleable ? (
<details class="toggle">
<summary>
<a href={`#${id}`} id={id}>
<h3>
{block.Heading1.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h3>
</a>
</summary>
<div>
{block.Heading1.Children && (
<NotionBlocks blocks={block.Heading1.Children} headings={headings} />
)}
</div>
</details>
) : (
<a href={`#${id}`} id={id}>
<h3>
{block.Heading1.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h3>
</a>
)
}
<style>
h3 {
margin: 1.1em 0 0.3em;
color: var(--fg);
font-size: 1.8rem;
}
@media (max-width: 640px) {
h3 {
font-size: 1.3rem;
}
}
.toggle {
margin: 2rem 0 0;
}
@media (max-width: 640px) {
.toggle {
margin: 1.4rem 0 0;
}
}
.toggle > summary {
cursor: pointer;
}
.toggle > summary > a {
display: inline;
}
.toggle > summary > a > h3 {
display: inline;
}
.toggle > div {
margin-left: 1em;
}
</style>

View File

@@ -0,0 +1,86 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { buildHeadingId } from '../../lib/blog-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Heading2) {
return null
}
const id = buildHeadingId(block.Heading2)
---
{
block.Heading2.IsToggleable ? (
<details class="toggle">
<summary>
<a href={`#${id}`} id={id}>
<h4>
{block.Heading2.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h4>
</a>
</summary>
<div>
{block.Heading2.Children && (
<NotionBlocks blocks={block.Heading2.Children} headings={headings} />
)}
</div>
</details>
) : (
<a href={`#${id}`} id={id}>
<h4>
{block.Heading2.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h4>
</a>
)
}
<style>
h4 {
margin: 1em 0 0.3em;
color: var(--fg);
font-size: 1.5rem;
}
@media (max-width: 640px) {
h4 {
font-size: 1.2rem;
}
}
.toggle {
margin: 1.6rem 0 0;
}
@media (max-width: 640px) {
.toggle {
margin: 1.2rem 0 0;
}
}
.toggle > summary {
cursor: pointer;
}
.toggle > summary > a {
display: inline;
}
.toggle > summary > a > h4 {
display: inline;
}
.toggle > div {
margin-left: 1em;
}
</style>

View File

@@ -0,0 +1,86 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { buildHeadingId } from '../../lib/blog-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Heading3) {
return null
}
const id = buildHeadingId(block.Heading3)
---
{
block.Heading3.IsToggleable ? (
<details class="toggle">
<summary>
<a href={`#${id}`} id={id}>
<h5>
{block.Heading3.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h5>
</a>
</summary>
<div>
{block.Heading3.Children && (
<NotionBlocks blocks={block.Heading3.Children} headings={headings} />
)}
</div>
</details>
) : (
<a href={`#${id}`} id={id}>
<h5>
{block.Heading3.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</h5>
</a>
)
}
<style>
h5 {
margin: 0.9em 0 0.3em;
color: var(--fg);
font-size: 1.25rem;
}
@media (max-width: 640px) {
h5 {
font-size: 1.1rem;
}
}
.toggle {
margin: 1.2rem 0 0;
}
@media (max-width: 640px) {
.toggle {
margin: 1.1rem 0 0;
}
}
.toggle > summary {
cursor: pointer;
}
.toggle > summary > a {
display: inline;
}
.toggle > summary > a > h5 {
display: inline;
}
.toggle > div {
margin-left: 1em;
}
</style>

View File

@@ -0,0 +1,58 @@
---
import { ENABLE_LIGHTBOX } from '@/consts'
import * as interfaces from '../../lib/interfaces'
import { filePath } from '../../lib/blog-helpers'
import Caption from './Caption.astro'
import fslightbox from 'fslightbox'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.Image) {
return null
}
let image = ''
if (block.Image.External) {
image = block.Image.External.Url
} else if (block.Image.File?.Url) {
image = filePath(new URL(block.Image.File.Url))
}
---
<figure class="image">
{
image && (
<div>
<div>
{ENABLE_LIGHTBOX ? (
<a data-fslightbox href={image} data-type="image">
<img src={image} alt="Image in a image block" loading="lazy" />
</a>
) : (
<img src={image} alt="Image in a image block" loading="lazy" />
)}
</div>
<Caption richTexts={block.Image.Caption} />
</div>
)
}
</figure>
<style>
.image {
display: flex;
margin: 0.2rem auto 0;
}
.image > div {
margin: 0 auto;
}
.image > div > div {
}
.image > div > div img {
display: block;
max-width: 100%;
}
</style>

View File

@@ -0,0 +1,129 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
const id = url.pathname.split('/')[2]
---
<blockquote
class="instagram-media"
data-instgrm-captioned
data-instgrm-permalink={`https://www.instagram.com/reel/${id}/?utm_source=ig_embed&utm_campaign=loading`}
data-instgrm-version="14"
style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"
>
<div style="padding:16px;">
<a
href={`https://www.instagram.com/reel/${id}/?utm_source=ig_embed&utm_campaign=loading`}
style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;"
target="_blank"
>
<div style=" display: flex; flex-direction: row; align-items: center;">
<div
style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"
>
</div>
<div
style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"
>
<div
style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"
>
</div>
<div
style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"
>
</div>
</div>
</div><div style="padding: 19% 0;"></div>
<div style="display:block; height:50px; margin:0 auto 12px; width:50px;">
<svg
width="50px"
height="50px"
viewBox="0 0 60 60"
version="1.1"
xmlns="https://www.w3.org/2000/svg"
xmlns:xlink="https://www.w3.org/1999/xlink"
><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"
><g transform="translate(-511.000000, -20.000000)" fill="#000000"
><g
><path
d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"
></path></g
></g
></g
></svg
>
</div><div style="padding-top: 8px;">
<div
style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;"
>
View this post on Instagram
</div>
</div><div style="padding: 12.5% 0;"></div>
<div
style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"
>
<div>
<div
style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"
>
</div>
<div
style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"
>
</div>
<div
style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"
>
</div>
</div><div style="margin-left: 8px;">
<div
style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"
>
</div>
<div
style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"
>
</div>
</div><div style="margin-left: auto;">
<div
style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"
>
</div>
<div
style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"
>
</div>
<div
style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"
>
</div>
</div>
</div>
<div
style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"
>
<div
style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"
>
</div>
<div
style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"
>
</div>
</div></a
><p
style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"
>
<a
href={`https://www.instagram.com/reel/${id}/?utm_source=ig_embed&utm_campaign=loading`}
style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;"
target="_blank">A post shared by Instagram</a
>
</p>
</div>
</blockquote>
<script async src="//www.instagram.com/embed.js"></script>

View File

@@ -0,0 +1,18 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import Mention from './Mention.astro'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.LinkToPage) {
return null
}
---
<p>
<Mention pageId={block.LinkToPage.PageId} />
</p>

View File

@@ -0,0 +1,87 @@
---
import type { Post } from '../../lib/interfaces.ts'
import { getPostByPageId } from '../../lib/notion/client'
import { getPostLink } from '../../lib/blog-helpers.ts'
import '../../styles/notion-color.css'
import arrow from '../../images/icon-arrow-link.svg'
export interface Props {
pageId: string
}
const { pageId } = Astro.props
let post: Post | null = null
if (pageId) {
post = await getPostByPageId(pageId)
}
---
{
post ? (
<a href={getPostLink(post.Slug)} class="link">
<>
<span class="icon">
{post.Icon && post.Icon.Type === 'emoji'
? post.Icon.Emoji
: post.Icon && post.Icon.Type === 'external'
? (
<img
src={post.Icon.Url}
class="notion-icon"
alt="Post title icon in a page link"
/>
)
: '📄'}
<img src={arrow.src} class="icon-link" alt="Arrow icon of a page link" />
</span>
<span class="text">{post.Title}</span>
</>
</a>
) : (
<a class="link">
<span class="icon">
🚫
<img src={arrow.src} class="icon-link" alt="Arrow icon of a page link" />
</span>
<span class="text not-found">Post not found</span>
</a>
)
}
<style>
a.link {
display: inline-flex;
font-weight: 600;
gap: 4px;
}
span.icon {
height: fit-content;
flex-shrink: 0;
position: relative;
}
span.icon img.notion-icon {
width: 1.3em;
height: 1.3rem;
vertical-align: sub;
flex-shrink: 0;
position: relative;
}
span.icon img.icon-link {
display: block;
position: absolute;
top: 1em;
right: 0;
width: 8px;
height: 8px;
}
span.text {
color: var(--fg);
font-weight: 500;
text-decoration: underline;
}
span.text.not-found {
font-weight: normal;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,51 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
level?: number
headings: interfaces.Block[]
}
const { block, level = 0, headings }: Props = Astro.props
const listTypes = ['i', '1', 'a'] as const
const items = block.ListItems ?? []
---
<ol type={listTypes[level % listTypes.length]}>
{
items
.filter(
(b: interfaces.Block) => b.Type === 'numbered_list_item' && b.NumberedListItem,
)
.map((b: interfaces.Block) => {
const item = b.NumberedListItem
if (!item) return null
return (
<li class={snakeToKebab(item.Color)}>
{item.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
{item.Children && item.Children.length > 0 && (
<NotionBlocks
blocks={item.Children}
level={level + 1}
headings={headings}
/>
)}
</li>
)
})
}
</ol>
<style>
ol {
font-size: 1rem;
}
</style>

View File

@@ -0,0 +1,39 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Paragraph) {
return null
}
---
<p class={snakeToKebab(block.Paragraph.Color)}>
{
block.Paragraph.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))
}
{
block.Paragraph.Children && (
<NotionBlocks blocks={block.Paragraph.Children} headings={headings} />
)
}
</p>
<style>
p {
margin: 0.3rem 0;
font-size: 1rem;
min-height: 1.8rem;
}
</style>

View File

@@ -0,0 +1,15 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
---
<a href={url.toString()} data-pin-do="embedPin"></a>
<script
type="text/javascript"
async
defer
src="//assets.pinterest.com/js/pinit.js"
></script>

View File

@@ -0,0 +1,41 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Quote) {
return null
}
---
<blockquote class={snakeToKebab(block.Quote.Color)}>
{
block.Quote.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))
}
{
block.Quote.Children && (
<NotionBlocks blocks={block.Quote.Children} headings={headings} />
)
}
</blockquote>
<style>
blockquote {
margin: 0.6rem 0;
padding: 0 0.9rem;
border-left: 3px solid var(--fg);
font-size: 1rem;
line-height: 1.8rem;
}
</style>

View File

@@ -0,0 +1,72 @@
---
import katex from 'katex'
import type { RichText } from '../../lib/interfaces.ts'
import Bold from './annotations/Bold.astro'
import Italic from './annotations/Italic.astro'
import Strikethrough from './annotations/Strikethrough.astro'
import Underline from './annotations/Underline.astro'
import Color from './annotations/Color.astro'
import Code from './annotations/Code.astro'
import Anchor from './annotations/Anchor.astro'
import Mention from './Mention.astro'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
<Anchor richText={richText}>
{
(
<Code richText={richText}>
{
<Color richText={richText}>
{
<Underline richText={richText}>
{
<Strikethrough richText={richText}>
{
<Italic richText={richText}>
{
<Bold richText={richText}>
{richText.Text &&
richText.Text.Content.split('\n').map(
(content: string, i: number) => {
if (i === 0) {
return content
}
return (
<>
<br />
{content}
</>
)
}
)}
{richText.Equation && (
<span
set:html={katex.renderToString(
richText.Equation.Expression,
{ throwOnError: false }
)}
/>
)}
{richText.Mention && richText.Mention.Page && (
<Mention pageId={richText.Mention.Page.Id} />
)}
</Bold>
}
</Italic>
}
</Strikethrough>
}
</Underline>
}
</Color>
}
</Code>
)
}
</Anchor>

View File

@@ -0,0 +1,17 @@
---
import NotionBlocks from '../NotionBlocks.astro'
import type * as interfaces from '../../lib/interfaces'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.SyncedBlock || !block.SyncedBlock.Children) {
return null
}
---
<NotionBlocks blocks={block.SyncedBlock.Children} headings={headings} />

View File

@@ -0,0 +1,60 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import RichText from './RichText.astro'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.Table) {
return null
}
---
<div class="table">
<table>
<tbody>
{
block.Table.Rows.map((tableRow: interfaces.TableRow, j: number) => (
<tr>
{tableRow.Cells.map((cell: interfaces.TableCell, i: number) => {
if (
(block.Table.HasRowHeader && i === 0) ||
(block.Table.HasColumnHeader && j === 0)
) {
return (
<th>
{cell.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</th>
)
}
return (
<td>
{cell.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))}
</td>
)
})}
</tr>
))
}
</tbody>
</table>
</div>
<style>
.table {
}
.table table {
margin: 0.6rem 0;
}
.table th,
.table td {
font-weight: normal;
}
</style>

View File

@@ -0,0 +1,69 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { buildHeadingId } from '../../lib/blog-helpers.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.TableOfContents) {
return null
}
---
<div class="table-of-contents">
{
headings.map((headingBlock: interfaces.Block) => {
const heading =
headingBlock.Heading1 || headingBlock.Heading2 || headingBlock.Heading3
if (!heading) return null
let indentClass = ''
if (headingBlock.Type === 'heading_2') {
indentClass = 'indent-1'
} else if (headingBlock.Type === 'heading_3') {
indentClass = 'indent-2'
}
return (
<a
href={`#${buildHeadingId(heading)}`}
class={`table-of-contents ${snakeToKebab(
block.TableOfContents.Color,
)} ${indentClass}`}
>
{heading.RichTexts.map(
(richText: interfaces.RichText) => richText.PlainText
).join('')}
</a>
)
})
}
</div>
<style>
.table-of-contents {
}
.table-of-contents > a {
display: block;
line-height: 1.8rem;
font-size: 0.9rem;
font-weight: 500;
text-decoration: underline;
}
.table-of-contents > a:hover {
background: rgba(241, 241, 239, 1) !important;
}
.table-of-contents > a.indent-1 {
padding-left: 1.5rem;
}
.table-of-contents > a.indent-2 {
padding-left: 3rem;
}
</style>

View File

@@ -0,0 +1,40 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
const user = url.pathname.split('/')[1]
const videoId = url.pathname.split('/')[3]
---
<div class="tiktok-wrapper">
<blockquote
class="tiktok-embed"
cite={url.toString()}
data-video-id={videoId}
style="min-width: 325px;"
>
<section>
<a
target="_blank"
title={user}
href="https://www.tiktok.com/{user}?refer=embed">{user}</a
>
</section>
</blockquote>
</div>
<script async src="https://www.tiktok.com/embed.js"></script>
<style>
.tiktok-wrapper {
max-width: 325px;
overflow-x: auto;
margin-block-start: 1.5rem;
margin-inline: auto;
border-radius: 8px;
}
blockquote.tiktok-embed {
margin: 0;
}
</style>

View File

@@ -0,0 +1,67 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
const todos =
block.ListItems?.filter(
(b: interfaces.Block) => b.Type === 'to_do' && b.ToDo,
) ?? (block.ToDo && block.Type === 'to_do' ? [block] : [])
if (todos.length === 0) {
return null
}
---
<div class="to-do">
{
todos.map((todoBlock: interfaces.Block) => {
const todo = todoBlock.ToDo
if (!todo) return null
return (
<div class={snakeToKebab(todo.Color)}>
<input type="checkbox" checked={todo.Checked} disabled />
{todo.RichTexts.map((richText: interfaces.RichText) => {
if (todo.Checked) {
return (
<s>
<RichText richText={richText} />
</s>
)
}
return <RichText richText={richText} />
})}
{todo.Children && (
<NotionBlocks blocks={todo.Children} headings={headings} />
)}
</div>
)
})
}
</div>
<style>
.to-do {
color: #222;
font-weight: 400;
font-size: 1rem;
line-height: 1.8rem;
padding-inline-start: 1rem;
}
.to-do > div {
}
.to-do > div > input {
}
.to-do > div > s {
color: var(--accents-3);
}
</style>

View File

@@ -0,0 +1,49 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import RichText from './RichText.astro'
import NotionBlocks from '../NotionBlocks.astro'
import '../../styles/notion-color.css'
export interface Props {
block: interfaces.Block
headings: interfaces.Block[]
}
const { block, headings }: Props = Astro.props
if (!block.Toggle) {
return null
}
---
<details class={`toggle ${snakeToKebab(block.Toggle.Color)}`}>
<summary>
{
block.Toggle.RichTexts.map((richText: interfaces.RichText) => (
<RichText richText={richText} />
))
}
</summary>
<div>
<NotionBlocks blocks={block.Toggle.Children} headings={headings} />
</div>
</details>
<style>
.toggle {
padding: 0.4rem;
}
.toggle > summary {
cursor: pointer;
}
.toggle > summary > a {
display: inline;
}
.toggle > div {
margin-left: 1em;
}
</style>

View File

@@ -0,0 +1,31 @@
---
export interface Props {
url: URL
}
const { url } = Astro.props
const postURL =
url.hostname === 'x.com' || url.hostname === 'www.x.com'
? new URL(url.pathname, 'https://twitter.com')
: url
---
<div class="tweet-embed">
<blockquote class="twitter-tweet">
<a href={postURL}></a>
</blockquote>
</div>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<style>
.tweet-embed {
width: 100%;
max-width: 640px;
margin: auto;
}
.tweet-embed div:first-child div:first-child {
margin: auto;
}
</style>

View File

@@ -0,0 +1,59 @@
---
import * as interfaces from '../../lib/interfaces.ts'
import { isYouTubeURL, parseYouTubeVideoId } from '../../lib/blog-helpers.ts'
import Caption from './Caption.astro'
export interface Props {
block: interfaces.Block
}
const { block }: Props = Astro.props
if (!block.Video) {
return null
}
let url: URL
try {
if (block.Video.External?.Url) {
url = new URL(block.Video.External.Url)
} else {
throw new Error('Missing video URL')
}
} catch (err) {
console.log(err)
url = null
}
---
<div class="video">
<div>
{
url && isYouTubeURL(url) && (
<iframe
src={`https://www.youtube.com/embed/${parseYouTubeVideoId(url)}`}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
/>
)
}
</div>
<Caption richTexts={block.Video.Caption} />
</div>
<style>
.video div:first-child {
width: 100%;
}
.video div:first-child iframe {
width: 100%;
height: 340px;
}
@media (max-width: 640px) {
.video div:first-child iframe {
height: 220px;
}
}
</style>

View File

@@ -0,0 +1,24 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Href && !richText.Mention ? (
<a href={richText.Href}><slot /></a>
) : (
<slot />
)
}
<style>
a {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,18 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Bold ? (
<b><slot /></b>
) : (
<slot />
)
}

View File

@@ -0,0 +1,25 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Code ? (
<code><slot /></code>
) : (
<slot />
)
}
<style>
code {
color: #eb5757;
padding: 0.25rem;
}
</style>

View File

@@ -0,0 +1,20 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
import { snakeToKebab } from '../../../lib/style-helpers.ts'
import '../../../styles/notion-color.css'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Color && richText.Annotation.Color !== 'default' ? (
<span class={snakeToKebab(richText.Annotation.Color)}><slot /></span>
) : (
<slot />
)
}

View File

@@ -0,0 +1,18 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Italic ? (
<i><slot /></i>
) : (
<slot />
)
}

View File

@@ -0,0 +1,18 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Strikethrough ? (
<s><slot /></s>
) : (
<slot />
)
}

View File

@@ -0,0 +1,18 @@
---
import type { RichText } from '../../../lib/interfaces.ts'
export interface Props {
richText: RichText
}
const { richText } = Astro.props
---
{
/* prettier-ignore */
richText.Annotation.Underline ? (
<u><slot /></u>
) : (
<slot />
)
}

View File

@@ -1,5 +1,7 @@
import type { IconMap, SocialLink, Site } from '@/types' import type { IconMap, SocialLink, Site } from '@/types'
export const ENABLE_LIGHTBOX = true
export const SITE: Site = { export const SITE: Site = {
title: '溴化锂的笔记本', title: '溴化锂的笔记本',
description: description:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,10 +0,0 @@
---
title: '2023 Post'
description: 'This is a dummy post written in the year 2023.'
date: 2023-06-01
tags: ['v1.0.0']
image: './banner.png'
authors: ['enscribe']
---
This is a dummy post written in the year 2023.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,154 +0,0 @@
---
title: '2024 Post'
description: 'This is a dummy post written in the year 2024 (with multiple authors).'
date: 2024-06-01
tags: ['v1.0.0']
image: './banner.png'
authors: ['enscribe', 'jktrn']
---
This is a dummy post written in the year 2024! Here is a long blog post with heavily nested headers, which can be used to test the table of contents:
## Test 2
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
##### Test 5
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
##### Test 5
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
##### Test 5
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
##### Test 5
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
###### Test 6
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
#### Test 4
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
##### Test 5
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
### Test 3
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -1,442 +0,0 @@
---
title: 'v1.5.0: “A Callout Component for Nerds”'
description: 'A quick update introduces our first content-based component: the callout!'
date: 2025-04-24
tags: ['v1.5.0']
image: './banner.png'
authors: ['enscribe']
---
import Callout from '@/components/Callout.astro'
import { Icon } from 'astro-icon/components'
## Our (hesitantly) first content-based component
This new version of astro-erudite, v1.5.0, introduces our first content-based component: the callout! I was a bit hesitant about adding this component because, frankly, the entire philosophy behind this project was to be as minimalistic as possible—I wanted to simply provide boilerplate to remove the "busy work" factor that often takes away from the writing process.
However, just based on some blog posts I've seen that use this template, I felt like there would be a universal desire just to have this component around in the case where it'd be needed. The primary inspiration came when user [@rezaarezvan](https://github.com/rezaarezvan) sent in a PR to add their site, [rezaarezvan.com](https://rezaarezvan.com), to the examples section in the README. They had years upon years of accumulated notes and resources on their site, most of which were in the form of LaTeX-style academic content that requires "blocks" for things like definitions, theorems, and proofs. I sent in an encouraging reply to the PR and then started building the component:
> [[@jktrn]](https://github.com/jktrn/astro-erudite/pull/29#issuecomment-2814894034) your blog posts are literally insane btw @rezaarezvan how have you written multiple textbooks worth of educational resources???
>
> i think i might add latex-style theorem/lemma/corollary/def/proof/eg/ex/remark blocks to astro-erudite so i can accommodate for these academia-style blogs, like e.g. for exercises it'd just be a component with an expandable section to hide the answer
### How does it work?
I've added a simple `Callout.astro` to `src/components` that now comes shipped with the template. It's a very easy-to-read component that has an insanely long configuration scheme for all of the different types of callouts that I've added. Fundamentally, it follows the same paradigm as [shadcn/ui](https://ui.shadcn.com) which uses [class-variance-authority](https://cva.style/docs) to create different "variants" on top of a base styling scheme:
```astro title="src/components/Callout.astro" collapse={12-126} {8-10,131}
---
import { cn } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { cva, type VariantProps } from 'class-variance-authority'
const calloutConfig = {
note: {
style: 'border-blue-500 dark:bg-blue-950/5',
textColor: 'text-blue-700 dark:text-blue-300',
icon: 'lucide:info',
},
tip: {
style: 'border-green-500 dark:bg-green-950/5',
textColor: 'text-green-700 dark:text-green-300',
icon: 'lucide:lightbulb',
},
warning: {
style: 'border-amber-500 dark:bg-amber-950/5',
textColor: 'text-amber-700 dark:text-amber-300',
icon: 'lucide:alert-triangle',
},
danger: {
style: 'border-red-500 dark:bg-red-950/5',
textColor: 'text-red-700 dark:text-red-300',
icon: 'lucide:shield-alert',
},
important: {
style: 'border-purple-500 dark:bg-purple-950/5',
textColor: 'text-purple-700 dark:text-purple-300',
icon: 'lucide:message-square-warning',
},
definition: {
style: 'border-purple-500 dark:bg-purple-950/5',
textColor: 'text-purple-700 dark:text-purple-300',
icon: 'lucide:book-open',
},
theorem: {
style: 'border-teal-500 dark:bg-teal-950/5',
textColor: 'text-teal-700 dark:text-teal-300',
icon: 'lucide:check-circle',
},
lemma: {
style: 'border-sky-400 dark:bg-sky-950/5',
textColor: 'text-sky-700 dark:text-sky-300',
icon: 'lucide:puzzle',
},
proof: {
style: 'border-gray-500 dark:bg-gray-950/5',
textColor: 'text-gray-700 dark:text-gray-300',
icon: 'lucide:check-square',
},
corollary: {
style: 'border-cyan-500 dark:bg-cyan-950/5',
textColor: 'text-cyan-700 dark:text-cyan-300',
icon: 'lucide:git-branch',
},
proposition: {
style: 'border-slate-500 dark:bg-slate-950/5',
textColor: 'text-slate-700 dark:text-slate-300',
icon: 'lucide:file-text',
},
axiom: {
style: 'border-violet-600 dark:bg-violet-950/5',
textColor: 'text-violet-700 dark:text-violet-300',
icon: 'lucide:anchor',
},
conjecture: {
style: 'border-pink-500 dark:bg-pink-950/5',
textColor: 'text-pink-700 dark:text-pink-300',
icon: 'lucide:help-circle',
},
notation: {
style: 'border-slate-400 dark:bg-slate-950/5',
textColor: 'text-slate-700 dark:text-slate-300',
icon: 'lucide:pen-tool',
},
remark: {
style: 'border-gray-400 dark:bg-gray-950/5',
textColor: 'text-gray-700 dark:text-gray-300',
icon: 'lucide:message-circle',
},
intuition: {
style: 'border-yellow-500 dark:bg-yellow-950/5',
textColor: 'text-yellow-700 dark:text-yellow-300',
icon: 'lucide:lightbulb',
},
recall: {
style: 'border-blue-300 dark:bg-blue-950/5',
textColor: 'text-blue-600 dark:text-blue-300',
icon: 'lucide:rotate-ccw',
},
explanation: {
style: 'border-lime-500 dark:bg-lime-950/5',
textColor: 'text-lime-700 dark:text-lime-300',
icon: 'lucide:help-circle',
},
example: {
style: 'border-emerald-500 dark:bg-emerald-950/5',
textColor: 'text-emerald-700 dark:text-emerald-300',
icon: 'lucide:code',
},
exercise: {
style: 'border-indigo-500 dark:bg-indigo-950/5',
textColor: 'text-indigo-700 dark:text-indigo-300',
icon: 'lucide:dumbbell',
},
problem: {
style: 'border-orange-600 dark:bg-orange-950/5',
textColor: 'text-orange-700 dark:text-orange-300',
icon: 'lucide:alert-circle',
},
answer: {
style: 'border-teal-500 dark:bg-teal-950/5',
textColor: 'text-teal-700 dark:text-teal-300',
icon: 'lucide:check',
},
solution: {
style: 'border-emerald-600 dark:bg-emerald-950/5',
textColor: 'text-emerald-700 dark:text-emerald-300',
icon: 'lucide:check-circle-2',
},
summary: {
style: 'border-sky-500 dark:bg-sky-950/5',
textColor: 'text-sky-700 dark:text-sky-300',
icon: 'lucide:list',
},
} as const
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
const calloutVariants = cva('relative px-4 py-3 my-6 border-l-4 text-sm', {
variants: {
variant: Object.fromEntries(
Object.entries(calloutConfig).map(([key, config]) => [key, config.style]),
),
},
defaultVariants: {
variant: 'note',
},
})
```
As such, if you feel like there's any variant that you're missing, it's insanely trivial to add it yourself. It's less trivial, however, to figure out what colors to use since I've essentially taken up every good Tailwind color for these!
### How do I use these?
Within any `src/content/blog/**/*.mdx` file, you can now use the `Callout` component by importing it like so underneath your frontmatter:
```mdx title="src/content/blog/callouts-component/index.mdx" add={10}
---
title: 'v1.5.0: “A Callout Component for Nerds”'
description: 'A quick update introduces our first content-based component: the callout!'
date: 2025-04-24
tags: ['v1.5.0']
image: './1200x630.png'
authors: ['enscribe']
---
import Callout from '@/components/Callout.astro'
```
Then, you can use the component like so. This is just an example but you should actually read the text since it's relevant to the article:
````mdx showLineNumbers=false collapse={3-11}
<Callout title="About Github-flavored alerts" variant="important">
I know that Github typically uses the following syntax for "alerts" (which is what they call these callouts, you can see their documentation [here](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)):
```md showLineNumbers=false
> [!NOTE]
> Useful information that users should know, even when skimming content.
```
The above syntax is supposed to render like this:
<Callout>
Useful information that users should know, even when skimming content.
</Callout>
I believe they do this so they can keep you within the Markdown-like syntax system. However, since this is MDX, I thought we'd be better off just using the component paradigm since it's a bit impractical to make a rehype plugin that can parse this form of syntax (also, solutions for this already exist, such as [lin-stephanie/rehype-callouts](https://github.com/lin-stephanie/rehype-callouts)). Additionally, I find it a pain to work with the `>{:md}` (the `<blockquote>{:html}` indicators for Markdown) syntax, as it makes it difficult to nest things such as code blocks within them in pure Markdown. We'll use components for now!
</Callout>
````
<Callout title="About Github-flavored alerts" variant="important">
I know that Github typically uses the following syntax for "alerts" (which is what they call these callouts, you can see their documentation [here](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)):
```md showLineNumbers=false
> [!NOTE]
> Useful information that users should know, even when skimming content.
```
The above syntax is supposed to render like this:
<Callout>
Useful information that users should know, even when skimming content.
</Callout>
I believe they do this so they can keep you within the Markdown-like syntax system. However, since this is MDX, I thought we'd be better off just using the component paradigm since it's a bit impractical to make a rehype plugin that can parse this form of syntax (also, solutions for this already exist, such as [lin-stephanie/rehype-callouts](https://github.com/lin-stephanie/rehype-callouts)). Additionally, I find it a pain to work with the `>{:md}` (the `<blockquote>{:html}` indicators for Markdown) syntax, as it makes it difficult to nest things such as code blocks within them in pure Markdown. We'll use components for now!
</Callout>
Callout only supports three props:
| Prop | Description | Default |
| ---- | ----------- | ------- |
| `title` | The title of the callout | `undefined{:js}` |
| `variant` | The variant of the callout | `"note"{:js}` |
| `defaultOpen` | Whether the callout `<details>{:html}` box is open by default | `true{:js}` |
I've added an insane amount of variants to this component for potentially any use case you could think of. For the more general ones, you can use the following:
<div class="[&_td]:align-middle">
| Variant | Usage |
| ------- | ----- |
| <span class="flex items-center gap-2 border-blue-500 dark:bg-blue-950/10 text-blue-700 dark:text-blue-300 px-2 py-1 border-l-4"><Icon name="lucide:info" /> Note</span> | For general information or comments that don't fit other categories |
| <span class="flex items-center gap-2 border-green-500 dark:bg-green-950/10 text-green-700 dark:text-green-300 px-2 py-1 border-l-4"><Icon name="lucide:lightbulb" /> Tip</span> | For helpful advice or shortcuts related to the topic at hand |
| <span class="flex items-center gap-2 border-amber-500 dark:bg-amber-950/10 text-amber-700 dark:text-amber-300 px-2 py-1 border-l-4"><Icon name="lucide:alert-triangle" /> Warning</span> | For potential pitfalls or common misconceptions |
| <span class="flex items-center gap-2 border-red-500 dark:bg-red-950/10 text-red-700 dark:text-red-300 px-2 py-1 border-l-4"><Icon name="lucide:shield-alert" /> Danger</span> | For things that could potentially be destructive or harmful |
| <span class="flex items-center gap-2 border-purple-500 dark:bg-purple-950/10 text-purple-700 dark:text-purple-300 px-2 py-1 border-l-4"><Icon name="lucide:message-square-warning" /> Important</span> | For things that are important to the reader's understanding |
</div>
For the potentially more academic/mathy folk, you can use the following. I've created specific variants that are tailor-made to be used alongside and/or nested into each other:
<div class="[&_td]:align-middle">
| Variant | Usage |
| ------- | ----- |
| <span class="flex items-center gap-2 border-purple-500 dark:bg-purple-950/10 text-purple-700 dark:text-purple-300 px-2 py-1 border-l-4"><Icon name="lucide:book-open" /> Definition</span> | For defining terms or concepts |
| <span class="flex items-center gap-2 border-teal-500 dark:bg-teal-950/10 text-teal-700 dark:text-teal-300 px-2 py-1 border-l-4"><Icon name="lucide:check-circle" /> Theorem</span> | For important mathematical or logical statements that have been proven |
| <span class="flex items-center gap-2 border-sky-400 dark:bg-sky-950/10 text-sky-700 dark:text-sky-300 px-2 py-1 border-l-4"><Icon name="lucide:puzzle" /> Lemma</span> | For helper theorems used in proving larger results |
| <span class="flex items-center gap-2 border-gray-500 dark:bg-gray-950/10 text-gray-700 dark:text-gray-300 px-2 py-1 border-l-4"><Icon name="lucide:check-square" /> Proof</span> | For logical arguments that establish the truth of a theorem or lemma |
| <span class="flex items-center gap-2 border-cyan-500 dark:bg-cyan-950/10 text-cyan-700 dark:text-cyan-300 px-2 py-1 border-l-4"><Icon name="lucide:git-branch" /> Corollary</span> | For results that follow directly from theorems |
| <span class="flex items-center gap-2 border-slate-500 dark:bg-slate-950/10 text-slate-700 dark:text-slate-300 px-2 py-1 border-l-4"><Icon name="lucide:file-text" /> Proposition</span> | For important statements that are less significant than theorems |
| <span class="flex items-center gap-2 border-violet-600 dark:bg-violet-950/10 text-violet-700 dark:text-violet-300 px-2 py-1 border-l-4"><Icon name="lucide:anchor" /> Axiom</span> | For fundamental assumptions that are accepted without proof |
| <span class="flex items-center gap-2 border-pink-500 dark:bg-pink-950/10 text-pink-700 dark:text-pink-300 px-2 py-1 border-l-4"><Icon name="lucide:help-circle" /> Conjecture</span> | For unproven statements believed to be true |
| <span class="flex items-center gap-2 border-slate-400 dark:bg-slate-950/10 text-slate-700 dark:text-slate-300 px-2 py-1 border-l-4"><Icon name="lucide:pen-tool" /> Notation</span> | For explaining mathematical notation |
| <span class="flex items-center gap-2 border-gray-400 dark:bg-gray-950/10 text-gray-700 dark:text-gray-300 px-2 py-1 border-l-4"><Icon name="lucide:message-circle" /> Remark</span> | For additional comments or (potentially out-of-scope) observations |
| <span class="flex items-center gap-2 border-yellow-500 dark:bg-yellow-950/10 text-yellow-700 dark:text-yellow-300 px-2 py-1 border-l-4"><Icon name="lucide:lightbulb" /> Intuition</span> | For explaining the intuitive reasoning behind concepts |
| <span class="flex items-center gap-2 border-blue-300 dark:bg-blue-950/10 text-blue-600 dark:text-blue-300 px-2 py-1 border-l-4"><Icon name="lucide:rotate-ccw" /> Recall</span> | For reminding readers of previously covered material |
| <span class="flex items-center gap-2 border-lime-500 dark:bg-lime-950/10 text-lime-700 dark:text-lime-300 px-2 py-1 border-l-4"><Icon name="lucide:help-circle" /> Explanation</span> | For providing deeper insights or clarifying complex topics |
| <span class="flex items-center gap-2 border-emerald-500 dark:bg-emerald-950/10 text-emerald-700 dark:text-emerald-300 px-2 py-1 border-l-4"><Icon name="lucide:code" /> Example</span> | For illustrating concepts with concrete examples or analogies |
| <span class="flex items-center gap-2 border-indigo-500 dark:bg-indigo-950/10 text-indigo-700 dark:text-indigo-300 px-2 py-1 border-l-4"><Icon name="lucide:dumbbell" /> Exercise</span> | For practice problems or take-home challenges for the reader |
| <span class="flex items-center gap-2 border-orange-600 dark:bg-orange-950/10 text-orange-700 dark:text-orange-300 px-2 py-1 border-l-4"><Icon name="lucide:alert-circle" /> Problem</span> | For presenting problems to be solved thoroughly |
| <span class="flex items-center gap-2 border-teal-500 dark:bg-teal-950/10 text-teal-700 dark:text-teal-300 px-2 py-1 border-l-4"><Icon name="lucide:check" /> Answer</span> | For providing simple, short answers to exercises or problems |
| <span class="flex items-center gap-2 border-emerald-600 dark:bg-emerald-950/10 text-emerald-700 dark:text-emerald-300 px-2 py-1 border-l-4"><Icon name="lucide:check-circle-2" /> Solution</span> | For detailed solutions to exercises or problems |
| <span class="flex items-center gap-2 border-sky-500 dark:bg-sky-950/10 text-sky-700 dark:text-sky-300 px-2 py-1 border-l-4"><Icon name="lucide:list" /> Summary</span> | For summarizing key points or concepts |
</div>
## Generic callouts
These are what the generic callouts look like (of course, all the text is made up):
<Callout title="Prerequisites for advanced React development" variant="note">
This tutorial assumes you're familiar with React hooks and the component lifecycle. If you need a refresher, check out the [official React documentation](https://reactjs.org/docs/hooks-intro.html) before proceeding.
</Callout>
<Callout title="Productivity enhancement" variant="tip">
You can quickly format your code by pressing <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>F</kbd> (Windows/Linux) or <kbd>Cmd</kbd> + <kbd>Option</kbd> + <kbd>F</kbd> (Mac).
Additional shortcuts include:
| Action | Windows/Linux | Mac |
| ------ | ------------- | --- |
| Search | <kbd>Ctrl</kbd> + <kbd>F</kbd> | <kbd>Cmd</kbd> + <kbd>F</kbd> |
| Replace | <kbd>Ctrl</kbd> + <kbd>H</kbd> | <kbd>Cmd</kbd> + <kbd>H</kbd> |
| Save all | <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>S</kbd> | <kbd>Cmd</kbd> + <kbd>Option</kbd> + <kbd>S</kbd> |
</Callout>
<Callout title="Cross-browser compatibility issues" variant="warning">
This API is **not supported** in Internet Explorer and has limited support in older browsers. Make sure to include appropriate polyfills.
```js title="polyfill.js"
if (!Object.fromEntries) {
Object.fromEntries = function(entries) {
const obj = {};
for (const [key, value] of entries) {
obj[key] = value;
}
return obj;
};
}
```
See the [Browser Compatibility Table](#) for detailed information.
</Callout>
<Callout title="Critical data loss risk" variant="danger">
Running this command will **permanently delete** all files in your current directory. Make sure to back up important data before proceeding.
```bash title="danger.sh"
# This will delete everything in the current directory
rm -rf ./*
# Safer alternative with confirmation
rm -ri ./*
```
This operation cannot be undone and recovery tools may not be able to restore your data.
</Callout>
<Callout title="Major API changes in v2.0" variant="important">
Version 2.0 introduces significant API changes. You'll need to update your existing code to use the new parameter structure.
1. The `configure(){:js}` method now returns a Promise
2. Authentication requires an API key object instead of a string
3. Event handlers use a new callback pattern
A migration example looks like the following:
```diff lang="js"
- app.configure("api-key-string");
+ await app.configure({ key: "api-key-string", version: "2.0" });
- app.on('event', callback);
+ app.on('event', { handler: callback, options: { once: true } });
```
</Callout>
## Mathematical callouts
Like I mentioned before, some of these variants are meant to be nested within each other. Take, for example, the following:
<Callout title="Continuous function" variant="definition">
A function $f: X \rightarrow Y$ between topological spaces is continuous if for every open set $V \subseteq Y$, the preimage $f^{-1}(V) \subseteq X$ is open in $X$.
<Callout title="Equivalent formulations" variant="explanation">
For metric spaces, continuity can be characterized by: $\forall \varepsilon > 0, \exists \delta > 0$ such that $d_X(x,y) < \delta \implies d_Y(f(x),f(y)) < \varepsilon$. This captures the intuition that points close to each other in $X$ map to points close to each other in $Y$.
</Callout>
</Callout>
<Callout title="Law of Large Numbers" variant="theorem">
Let $X_1, X_2, \ldots$ be a sequence of independent and identically distributed random variables with expected value $\mathbb{E}[X_i] = \mu < \infty$. Then for any $\varepsilon > 0$:
$$
\lim_{n \to \infty} P\left(\left|\frac{1}{n}\sum_{i=1}^{n}X_i - \mu\right| > \varepsilon\right) = 0
$$
<Callout title="Proof" variant="proof">
We'll prove this using Chebyshev's inequality. Let $S_n = \sum_{i=1}^{n}X_i$ and $\sigma^2 = \text{Var}(X_i)$. By Chebyshev's inequality:
$$
P\left(\left|\frac{S_n}{n} - \mu\right| \geq \varepsilon\right) \leq \frac{\text{Var}(S_n/n)}{\varepsilon^2}
$$
Since the variables are independent, we have:
$$
\text{Var}(S_n/n) = \frac{\text{Var}(S_n)}{n^2} = \frac{n\sigma^2}{n^2} = \frac{\sigma^2}{n}
$$
Substituting this into our inequality:
$$
P\left(\left|\frac{S_n}{n} - \mu\right| \geq \varepsilon\right) \leq \frac{\sigma^2}{n\varepsilon^2}
$$
As $n \to \infty$, the right side approaches 0, which proves the theorem.
</Callout>
</Callout>
<Callout title="Monotone Convergence Theorem" variant="lemma">
Let $(f_n)$ be a sequence of non-negative measurable functions such that $f_n(x) \leq f_{n+1}(x)$ for all $n \in \mathbb{N}$ and almost all $x$. Define $f(x) = \lim_{n \to \infty} f_n(x)$. Then:
$$
\lim_{n \to \infty} \int f_n \, d\mu = \int f \, d\mu
$$
<Callout title="Proof" variant="proof">
Let $g_n = f_n \cdot \chi_E$ where $E = \{x : f(x) < \infty\}$. By Fatou's lemma:
$$
\int f \, d\mu = \int \lim_{n \to \infty} g_n \, d\mu \leq \liminf_{n \to \infty} \int g_n \, d\mu = \liminf_{n \to \infty} \int f_n \, d\mu
$$
For the reverse inequality, note that $f_n \leq f$ for all $n$, so $\int f_n \, d\mu \leq \int f \, d\mu$. Taking the limit:
$$
\limsup_{n \to \infty} \int f_n \, d\mu \leq \int f \, d\mu
$$
Combining these inequalities:
$$
\int f \, d\mu \leq \liminf_{n \to \infty} \int f_n \, d\mu \leq \limsup_{n \to \infty} \int f_n \, d\mu \leq \int f \, d\mu
$$
Therefore, $\lim_{n \to \infty} \int f_n \, d\mu = \int f \, d\mu$.
</Callout>
</Callout>
I'll add this little subgenre of variant called "example-based" callouts that have more generally a question-answer format. For the "exercise-answer" pairing, the difference is that I'd recommend you default the answer to a closed state to hide the answer from any peeping readers until they've actually done the work themselves. Typically, the difference between an "Answer" and a "Solution" is that the answer basically just gives you the final answer, while the solution will show you the steps it takes to get to the answer:
<Callout title="Finding the derivative of a product function" variant="exercise">
Calculate the derivative of $f(x) = x^3 \sin(x)$ using the product rule.
<Callout variant="answer" defaultOpen={false}>
$$
\frac{d}{dx}[x^3 \sin(x)] = 3x^2 \sin(x) + x^3 \cos(x)
$$
</Callout>
</Callout>
<Callout title="Convergence of arithmetic means" variant="problem">
Prove that if a sequence $(a_n)$ converges to $L$, then the sequence of arithmetic means $(\frac{a_1 + a_2 + \ldots + a_n}{n})$ also converges to $L$.
<Callout title="Detailed proof" variant="solution">
Let $\varepsilon > 0$ be given. Since $(a_n)$ converges to $L$, there exists $N \in \mathbb{N}$ such that $|a_n - L| < \frac{\varepsilon}{2}$ for all $n \geq N$. Let $S_n = \frac{a_1 + a_2 + \ldots + a_n}{n}$ be the sequence of arithmetic means.
We can split $S_n$ as follows:
$$
S_n = \frac{a_1 + a_2 + \ldots + a_N + a_{N+1} + \ldots + a_n}{n}
$$
For $n > N$, we have:
$$
\begin{align*}
|S_n - L| &= \left|\frac{a_1 + a_2 + \ldots + a_n}{n} - L\right| \\
&= \left|\frac{a_1 + a_2 + \ldots + a_n - nL}{n}\right| \\
&= \left|\frac{(a_1 - L) + (a_2 - L) + \ldots + (a_n - L)}{n}\right| \\
&\leq \frac{|a_1 - L| + |a_2 - L| + \ldots + |a_N - L| + |a_{N+1} - L| + \ldots + |a_n - L|}{n}
\end{align*}
$$
Let $M = \max\{|a_1 - L|, |a_2 - L|, \ldots, |a_N - L|\}$. Then:
$$
|S_n - L| \leq \frac{NM + (n-N)\frac{\varepsilon}{2}}{n} = \frac{NM}{n} + \frac{n-N}{n} \cdot \frac{\varepsilon}{2}
$$
As $n \to \infty$, $\frac{NM}{n} \to 0$ and $\frac{n-N}{n} \to 1$. So for sufficiently large $n$, we have:
$$
|S_n - L| < \varepsilon
$$
Therefore, the sequence of arithmetic means converges to $L$.
</Callout>
</Callout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1,82 +0,0 @@
---
title: 'v1.6.0: "Mobile Navigation & Subposts"'
description: 'This new version introduces improved mobile navigation (via sticky table of contents) and the concept of "subposts."'
date: 2025-05-21
tags: ['v1.6.0']
image: './banner.png'
authors: ['enscribe']
---
## Two major improvements to the reading experience
astro-erudite's v1.6.0 brings about two significant enhancements that I've been wanting to implement for quite some time! The first addresses a longstanding mobile <abbr title="User Experience">UX</abbr> issue, while the second introduces a content organization paradigm that I find interesting.
### Mobile navigation
The original table of contents implementation was frankly inadequate for mobile users. While desktop users enjoyed a beautiful sticky sidebar with scroll-aware highlighting, mobile users were stuck with a basic collapsible `<details>{:html}` element that provided no indication of reading progress or current location within the post:
<div class="max-w-xs mx-auto">
![Old TOC implementation](assets/old-toc.png)
</div>
The new mobile navigation system introduces a sticky header that sits just below the main site navigation, featuring a circular progress indicator and dynamic section display. As you scroll through a post, the progress circle fills to show how far you've read, while the text updates to reflect which section you're currently viewing. Tapping on this header expands a comprehensive table of contents that mirrors the desktop experience but in a mobile-friendly format:
<div class="flex gap-4 flex-wrap justify-center [&_img,p]:m-0">
<div class="max-w-xs">
![New TOC implementation (closed)](assets/new-toc-closed.png)
</div>
<div class="max-w-xs">
![New TOC implementation (open)](assets/new-toc-open.png)
</div>
</div>
This gives users the same level of navigation control as if they were on desktop, in a very intuitive and mobile-friendly format.
### Subposts for hierarchical content organization
The second major feature is something I'm calling "subposts," a way to organize related content in a parent-child hierarchy within your blog. This concept came from when I was writing [this](https://enscribe.dev/blog/japan-retrospective) travel blog post on my personal site, where I essentially wanted a "subpost" for each day of the trip since it was way too long to fit into a single post.
Instead of creating separate blog posts in a "series" (and thus clogging up your blog post listings with a bunch of smaller, tangentially-related posts), you can now automatically establish a parent-child relationship between posts by creating a folder for your main topic with an `index.mdx` file as your parent post, then adding additional `.mdx` files in the same folder as subposts. For example, this very post demonstrates the feature by containing two subposts that explore the technical implementation details of each feature. On desktop, we display a `<SubpostsSidebar>{:tsx}` component on the right-hand side of the page that shows a list of all the subposts alongside the parent post:
<div class="flex gap-4 flex-wrap justify-center [&_img,p,.expressive-code]:m-0">
<div class="max-w-3xs border">
![Subposts listing](assets/subposts-listing.png)
</div>
<div class="max-w-3xs">
```bash showLineNumbers={false}
src/
content/
blog/
mobile-nav-and-subposts/
index.mdx
mobile-navigation.mdx
subposts.mdx
```
</div>
</div>
The file structure is intuitive: create a folder for your main topic with an `index.mdx` file as your parent post, then add additional `.mdx` files in the same folder as subposts. Astro's file-based routing handles the URL structure automatically, creating paths like `/blog/subposts` for the parent post and `/blog/subposts/mobile-navigation` and `/blog/subposts/subposts` for the subposts.
#### Enhanced navigation patterns
Of course, we need to adjust our `<PostNavigation>{:tsx}` component to support this new feature. Now, whenever we're reading a subpost, we now have the option to traverse between subposts or even upwards to the parent post:
<div class="border [&_img,p]:m-0">
![Subposts navigation](assets/subpost-navigation.png)
</div>
This is contextually aware, meaning that if you're reading a parent post (or a post with no children), then this component will only show adjacent parent-level posts.
#### Hint: this is great for technical content
The subposts feature particularly shines for technical content which is meant to educate. In a similar manner to a tutorial or a textbook, we can now fragment our content into more digestible and informative subposts which are easily traversable between each other and from the parent post, and the reader is now free to jump around to whichever subpost they're interested in.
This post itself serves as an example, since you're currently reading the parent post (which I've called the "Index" post in the `<Breadcrumb>{:tsx}` component) and the subposts are the two posts that I've written about the technical implementation details of each feature.
What's great about the way I engineered subposts is that it's fully backwards-compatible with blog posts written before this, so there's no need to define extra frontmatter metadata or manually establish the parent-child relationships between posts. It serves lovely on the <abbr title="Developer Experience">DX</abbr> side as well!
## Go ahead and read the subposts
On desktop, the `<SubpostsSidebar>{:tsx}` sticks to the right column on your screen, and you can click on any of the subposts to read them. On mobile, it will turn into a `<SubpostsHeader>{:tsx}` component that will appear underneath the sticky header, above the sticky table of contents we just added.

View File

@@ -1,152 +0,0 @@
---
title: 'Implementing sticky mobile navigation'
description: 'Technical deep-dive into the sticky mobile table of contents with progress tracking and smart section highlighting'
date: 2025-05-21
tags: ['v1.6.0']
authors: ['enscribe']
order: 1
---
import Callout from '@/components/Callout.astro'
The original mobile table of contents was a simple collapsible element that lived within the post content. This created several usability issues:
1. Users had no sense of how much content remained other than implying it based on the length of the browser scrollbar
2. Once you scrolled past the TOC, you lose the ability to quickly navigate to other sections of the post without scrolling back up to the inline TOC
3. Mobile users should have the exact same experience as desktop users in terms of navigation, and as of now the desktop experience was better (due to the sticky aside TOC)
## Building the sticky header system
Design-wise, the component is relatively simple, since it only includes a chevron to indicate expansion state, a circular progress indicator that fills as you scroll, and a dynamic text showing the current section (or a combination of sections, if multiple are visible at the same time).
### Live scroll highlighting sucks
One of the more interesting problems I encountered was how to handle the highlighting of sections as you scroll. Of course, this applies to both mobile and desktop versions, but in this update I changed the implementation of both.
A naive implementation of live scroll highlighting would simply use an `IntersectionObserver(){:js}` to watch for headers entering and exiting the viewport. The issue with this is that it doesn't highlight anything if headers are no longer visible in your viewport, regardless of whether you're in a section that "belongs" to a heading.
<Callout variant="example">
Say that we have a post with the following structure:
```markdown
## Part 1
[500 lines of content]
## Part 2
[500 lines of content]
```
If you were to scroll way past the first heading and was deep into the first section underneath it, the naive implementation would not highlight the first heading because it's no longer in your viewport. This is unintuitive and a poor user experience. In a perfect world, if we were to view 250 lines of Part 1 and 250 lines of Part 2, then we would see both headings highlighted in the TOC and not need to make a decision about which heading to highlight.
</Callout>
I used to rely on [jakelow/remark-sectionize](https://github.com/jake-low/remark-sectionize), a [remarkjs/remark](https://github.com/remarkjs/remark) plugin that retroactively generates `<section>{:html}` tags based on the headers in the generated HTML. This would have done the following conversion:
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 [&_img,p,.expressive-code]:m-0">
<div>
```markdown
# Forest elephants
## Introduction
In this section, we discuss the lesser known forest elephants.
## Habitat
Forest elephants do not live in trees but among them.
```
</div>
<div>
```html
<section>
<h1>Forest elephants</h1>
<section>
<h2>Introduction</h2>
<p>In this section, we discuss the lesser known forest elephants.</p>
</section>
<section>
<h2>Habitat</h2>
<p>Forest elephants do not live in trees but among them.</p>
</section>
</section>
```
</div>
</div>
However, this approach had pretty complicated issues involving section nesting and the fact that we didn't have any control over its output other than by patching it. So, I decided to opt for a home-grown solution.
### The concept of "jurisdictions"
The naming is interesting but I felt like it was the most intuitive to me. Basically, I created a system that assigns each heading a "territory" that extends from its position to the start of the next heading (or the end of the document):
```javascript title="src/components/TOCHeader.astro" startLineNumber={123}
function buildHeadingJurisdictions() {
headingElements = Array.from(
document.querySelectorAll('.prose h2, .prose h3, .prose h4, .prose h5, .prose h6')
)
jurisdictions = headingElements.map((heading, index) => {
const nextHeading = headingElements[index + 1]
return {
id: heading.id,
start: heading.offsetTop,
end: nextHeading ? nextHeading.offsetTop : document.body.scrollHeight
}
})
}
```
1. First, we collect all heading elements (`<h2>{:html}` through `<h6>{:html}`) from the document's prose content area using `.querySelectorAll(){:js}`.
2. For each heading, we create a jurisdiction object that contains the heading's `id`, the vertical position where this section begins (the heading's `offsetTop` value, which we name `start`), and the vertical position where this section ends (the `offsetTop` of the next heading or the bottom of the document if it's the last heading, which we name `end`).
This creates a map of "territories" that each heading controls. This is crucial for accurately tracking which jurisdictions are currently visible as the user scrolls, even when the actual heading element itself is no longer in view.
### The decision to show all visible sections
One of the more interesting decisions I made was to display all sections as comma-separated within the mobile TOC's unexpanded state. This manifests as follows:
<Callout variant="example">
Recall the previous example's scenario:
```markdown
## Part 1
[500 lines of content]
## Part 2
[500 lines of content]
```
If we saw 250 lines of Part 1 and 250 lines of Part 2, then the text snippet in the mobile TOC would read "Part 1, Part 2".
</Callout>
The temptation is to implement a "smart" selection algorithm, perhaps showing the section with the most visible content, or the one closest to the viewport center, or to show the "deepest," most specifically nested section. However, this creates numerous edge cases:
1. If you click to navigate to a short final section, it might never become the "primary" section because there isn't enough content below it to scroll it to the top of the viewport.
2. As you scroll between sections, a "smart" selector might switch which section it considers primary at seemingly arbitrary points, creating a jarring experience.
3. When your viewport shows roughly equal amounts of two sections, any selection algorithm becomes essentially arbitrary.
By showing all visible sections, we give users complete awareness of their position in the document, eliminate the edge cases mentioned above, and create predictable behavior.
## Progress indicator implementation
The circular progress indicator provides immediate visual feedback about reading progress without requiring any interaction:
```javascript title="src/components/TOCHeader.astro" startLineNumber={216}
function updateProgressCircle() {
if (!progressCircleElement) return
const scrollableDistance =
document.documentElement.scrollHeight - window.innerHeight
const scrollProgress =
scrollableDistance > 0
? Math.min(Math.max(window.scrollY / scrollableDistance, 0), 1)
: 0
progressCircleElement.style.strokeDashoffset = (
PROGRESS_CIRCLE_CIRCUMFERENCE *
(1 - scrollProgress)
).toString()
}
```
The progress is calculated as a ratio of current scroll position to total scrollable distance, then applied as a stroke-dashoffset to create the filling effect.

View File

@@ -1,161 +0,0 @@
---
title: 'Implementing file-based subpost routing'
description: 'How file-based hierarchical content organization works under the hood'
date: 2025-05-21
tags: ['v1.6.0']
authors: ['enscribe']
order: 2
---
import Callout from '@/components/Callout.astro'
The subposts feature leverages Astro's file-based routing to automatically detect parent-child relationships without any configuration. The entire implementation hinges on a simple observation: if a post ID contains a forward slash, it's a subpost.
```typescript title="src/lib/data-utils.ts" startLineNumber={161}
export function isSubpost(postId: string): boolean {
return postId.includes('/')
}
export function getParentId(subpostId: string): string {
return subpostId.split('/')[0]
}
```
This is a pretty elegant solution which requires no frontmatter configuration, no manual relationship mapping, and zero migration effort for existing posts.
## Navigation complexity
One of the more intricate parts of this update was rethinking navigation. The original `getAdjacentPosts(){:ts}` function assumed simple previous/next relationships. With subposts, we now have three distinct navigation contexts:
<Callout variant="example">
Consider this structure:
```
blog/
getting-started.mdx
react-tutorial/
index.mdx
components.mdx
state.mdx
advanced-patterns.mdx
```
Navigation depends on context:
- From `getting-started.mdx`: next goes to `react-tutorial/index.mdx`
- From `react-tutorial/components.mdx`: next goes to `state.mdx`, previous to `index.mdx`
- From `react-tutorial/state.mdx`: previous goes to `components.mdx`, parent goes to `index.mdx`
</Callout>
Here's how we handle this complexity:
```typescript title="src/lib/data-utils.ts" startLineNumber={40}
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
parent: CollectionEntry<'blog'> | null
}> {
const allPosts = await getAllPosts()
if (isSubpost(currentId)) {
const parentId = getParentId(currentId)
const parent = allPosts.find((post) => post.id === parentId) || null
// Get all sibling subposts
const posts = await getCollection('blog')
const subposts = posts
.filter(
(post) =>
isSubpost(post.id) &&
getParentId(post.id) === parentId &&
!post.data.draft
)
.sort((a, b) => a.data.date.valueOf() - b.data.date.valueOf())
const currentIndex = subposts.findIndex((post) => post.id === currentId)
return {
newer: currentIndex < subposts.length - 1
? subposts[currentIndex + 1]
: null,
older: currentIndex > 0
? subposts[currentIndex - 1]
: null,
parent,
}
}
// For parent posts, only navigate among other parent-level posts
const parentPosts = allPosts.filter((post) => !isSubpost(post.id))
const currentIndex = parentPosts.findIndex((post) => post.id === currentId)
return {
newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null,
older: currentIndex < parentPosts.length - 1
? parentPosts[currentIndex + 1]
: null,
parent: null,
}
}
```
As a TL;DR, subposts should only navigate among siblings and should be able to go up to their parent, while parent posts should only navigate among other parent-level posts.
## Other considerations
- The breadcrumb component required careful thought to handle three distinct cases:
```astro title="src/pages/blog/[...id].astro" startLineNumber={70}
<Breadcrumbs
items={[
{ href: '/blog', label: 'Blog', icon: 'lucide:library-big' },
...(isCurrentSubpost && parentPost
? [
{
href: `/blog/${parentPost.id}`,
label: parentPost.data.title,
icon: 'lucide:book-open',
},
{
href: `/blog/${currentPostId}`,
label: post.data.title,
icon: 'lucide:file-text',
},
]
: [
{
href: `/blog/${currentPostId}`,
label: post.data.title,
icon: 'lucide:book-open-text',
},
]),
]}
/>
```
We append `-text` to `book-open` or `file` (for parent posts and subposts, respectively) to indicate the active post by differentiating it from inactive icons which would lack the text within the icon.
- The main blog listing (alongside other listings, e.g. filtering by tags, filtering by author) should exclude subposts to avoid cluttering the feed:
```typescript title="src/lib/data-utils.ts" startLineNumber={7}
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
```
Without this filter, your blog listing would show every subpost as a top-level entry, defeating the purpose of hierarchical organization.
- Desktop and mobile require fundamentally different approaches for displaying the subpost hierarchy. On desktop, we have the luxury of a persistent sidebar. On mobile, screen real estate demands integration with the sticky header system. This required careful slot management in the top-level `Layout.astro`:
```astro title="src/layouts/Layout.astro"
<div class="bg-background/50 sticky top-0 z-50 border-b backdrop-blur-sm">
<Header />
<slot name="subposts-navigation" />
<slot name="table-of-contents" />
</div>
```
The order is semantic here since subposts navigation comes before table of contents, creating a logical hierarchy of navigation options from broad (which post/subpost) to specific (which section).
- Not much testing has been done for deep nesting but my assumption is that it shouldn't work. This is intentional to maintain simplicity, since at that point you might as well use a documentation site rather than a blogging site.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -1,89 +0,0 @@
---
title: 'v1.3.0: “Patches in Production”'
description: 'Whenever you depend on Node packages with missing maintainers, patching becomes a necessary evil.'
date: 2025-03-21
tags: ['v1.3.0']
image: './banner.png'
authors: ['enscribe']
---
## A problem (about dead maintainers)
This post talks about changes I've made to astro-erudite in v1.3.0!
I recently found myself caught between two syntax highlighting packages that I absolutely needed for astro-erudite. On one hand, the current template uses [rehype-pretty-code](https://rehype-pretty.pages.dev/) as its main syntax highlighting solution, but due to issues with its inherent implementation and missing features that I needed, I had created a bunch of custom transformers to make it do what I wanted, and the whole setup was getting unwieldy. I then discovered [Expressive Code](https://expressive-code.com/), which had everything I wanted out of the box—collapsible code sections, terminal and editor frames, gutter comments—it was perfect! Well, almost perfect.
The primary issue was that Expressive Code doesn't support inline syntax highlighting, which is non-negotiable for me since I need my inline code snippets to look as good as my code blocks (so I could do stuff like `console.log("Hello, world!".split('').reverse().join('')){:js}`). So I opened a feature request at [expressive-code/expressive-code#250](https://github.com/expressive-code/expressive-code/issues/250) and the maintainer seemed interested, saying they'd get around to it eventually. Implementing this feature is a lot easier said than done though, and I summarized it well in another thread:
> [@jktrn](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2619869436): [...] expressive-code is already interested in implementing inline code support, but it would be a bit nuanced to add since it has to:
>
> - allow existing plugins to continue working normally with block-level code (without breaking changes),
> - enable new plugins to explicitly declare support for inline code,
> - and provide ways for plugins to distinguish between inline and block-level code processing.
However, I needed a solution immediately. My first thought was to use both packages together—Expressive Code for block code and rehype-pretty-code for inline code. However, importing both at the same time caused everything to break spectacularly.
## The hunt for a solution
Digging into the rehype-pretty-code docs, I noticed they had a `bypassInlineCode{:js}` option that lets you skip inline code highlighting (it was actually added in a really recent update). But what I needed was the opposite, which would be a way to make it only handle inline code and bypass blocks entirely.
So I opened a feature request at [rehype-pretty/rehype-pretty-code#247](https://github.com/rehype-pretty/rehype-pretty-code/issues/247) for a theoretical `bypassBlockCode{:js}` option. I got no response, since the repository seemed unmaintained for a bit since it seems like the maintainer has moved onto other projects.
Fast forward a few months, and user [@kelvindecosta](https://github.com/kelvindecosta) comments on my issue:
> [[@kelvindecosta]](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2610536000): Hey [@jktrn](https://github.com/jktrn), did you figure out a workaround for this? I'm interested in setting this up alongside expressive-code.
After I replied that I hadn't figured out a workaround yet, they sent me a brilliantly hacky solution a couple days later:
> [[@kelvindecosta]](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2619666231): Hey again @jktrn, I have found an unconventional way to achieve this.
>
> If you're using pnpm or bun, you can use their patch functionality to customize the contents of the `node_modules/rehype-pretty-code` package.
>
> I only recently learned about this feature, and it is a good workaround for the time being. Here are the steps:
>
> 1. Run `pnpm patch rehype-pretty-code`. This will instruct you to edit the files in a certain directory.
> 2. Patch out the `isBlockCode{:js}` function to always return `false{:js}`. This will instruct the plugin to not process any block code elements.
> 3. Run `pnpm patch-commit <path/to/files>`. This will create a nice patches folder with the right changes.
## Performing surgery on node_modules
This happened to be exactly what I needed! I went into my `node_modules` directory and made the changes manually:
```js title="node_modules/rehype-pretty-code/dist/index.js" startLineNumber=18 ins={9} del={8}
function isInlineCode(element, parent, bypass = false) {
if (bypass) {
return false;
}
return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode";
}
function isBlockCode(element) {
return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code";
return false;
}
```
From here, I ran `npx patch-package rehype-pretty-code`, which created a `patches/rehype-pretty-code+0.14.1.patch` file with the changes I made:
```diff title="patches/rehype-pretty-code+0.14.1.patch"
--- a/node_modules/rehype-pretty-code/dist/index.js
+++ b/node_modules/rehype-pretty-code/dist/index.js
@@ -22,7 +22,7 @@ function isInlineCode(element, parent, bypass = false) {
return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode";
}
function isBlockCode(element) {
- return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code";
+ return false;
}
function getInlineCodeLang(meta, defaultFallbackLang) {
const placeholder = "\0";
```
This simple modification forces rehype-pretty-code to completely ignore block code elements by always returning `false{:js}` from the `isBlockCode{:js}` function. Now Expressive Code handles all block code formatting, while rehype-pretty-code still beautifully handles my inline code. And just like that, they're working in perfect harmony!
## Please don't perform surgery on your node_modules
Absolutely do not do this for production sites (your personal blog does not count = ̄ω ̄=). Directly patching node modules is generally discouraged because patches can break with updates and create maintenance headaches down the road.
But sometimes, when you're working at the bleeding edge of web development, temporary solutions like this become necessary. The better approach would be to just wait for Expressive Code to implement inline syntax highlighting. But, since it'll take a while for reasons aforementioned, I'll stick with my janky solution. This patch buys me time until either rehype-pretty-code gets maintained again and implements the feature properly, or Expressive Code adds inline code support.
In the meantime, astro-erudite now has both beautiful code blocks and inline syntax highlighting. And now it's available for all of you to use!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -1,246 +0,0 @@
---
title: 'The State of Static Blogs in 2024'
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
date: 2024-07-25
tags: ['v1.0.0']
image: './banner.png'
authors: ['enscribe']
---
## Introduction
Hello! My name is enscribe ([jktrn](https://github.com/jktrn) on GitHub), and I'm a fullstack web developer who has been fiddling with blogging platforms for a couple of years now. I run a blog at [enscribe.dev](https://enscribe.dev), where I write about cybersecurity and the capture-the-flag (CTF) scene.
I have a lot of opinions about what makes a great blogging template. As a cumulative result of all the slop, bullshit, and outright terrible design decisions I've had to deal with working with various templates and frameworks, I bring you [astro-erudite](https://github.com/jktrn/astro-erudite), which should hopefully bring a better developer and user experience in terms of ease of use, customization, and performance.
astro-erudite is written in Astro, a framework hyperoptimized for static content such as blogs. Aesthetically, it is also designed to be as boring as possible while still maintaining maximum functionality, as to allow for the freedom of the developer (or the designer they hire) to make their blog uniquely their own. Within the codebase of this template I've included many nuances that, in my opinion (and there will be many, many opinions here), make the developer experience significantly more pleasant. I've also _excluded_ many features that, frankly, you don't need.
## Welcoming some DX features
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers&mdash;however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utility colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
- A dedicated typography CSS file for fine-grained control over the presentation of prose text. Although [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) (a plugin that automatically styles any content surrounded by an `<article>{:html}` tag) offers a solution to this, you lose out on all of the control and often have to make overrides for undesirable output. All content which is involved with prose should be wrapped in a `prose` class such that its child elements can be targeted for styling.
- [Expressive Code](https://expressive-code.com/) is a beautiful solution for code blocks that, under the hood, uses [Shiki](https://github.com/shikijs/shiki) for syntax highlighting. Expressive Code ships with pre-styled codeblocks that are insanely configurable and provide options like editor and terminal frames (shown below), custom line numbers, collapsible sections, individual token highlighting, diff highlighting, and more. To use these for any provided codeblock, simply add any of the following props after the codeblock's backticks:
````mdx showLineNumbers=false collapse={2-42}
```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
// <- This codeblock starts at line 100!
// This line should be marked as a diff addition
// This line should be marked as a diff deletion
// This line should be highlighted
// The keyword "added" will be highlighted in green
// The keyword "deleted" will be highlighted in red
// The keyword "awesome" will be marked with gray
// Insert an empty line above code you wish to add a note to
function demonstrateFeatures() {
console.log('Hello world!')
return true
}
function obfuscateString(input) {
return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
}
function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
}
```
````
This results in a codeblock that looks like this:
```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
// <- This codeblock starts at line 100!
// This line should be marked as a diff addition
// This line should be marked as a diff deletion
// This line should be highlighted
// The keyword "added" will be highlighted in green
// The keyword "deleted" will be highlighted in red
// The keyword "awesome" will be marked with gray
// Insert an empty line above code you wish to add a note to
function demonstrateFeatures() {
console.log('Hello world!')
return true
}
function obfuscateString(input) {
return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
}
function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
}
```
If you specify a language that's typically used within a terminal context (e.g. `ps1`, `sh`, `console`, etc.) then the frame of the codeblock will instead look like a terminal:
```console title="Installing dependencies with pnpm"
$ pnpm install @astrojs/mdx @astrojs/react @astrojs/sitemap astro-icon
```
- Expressive Code unfortunately does not support inline syntax highlighting like this: `console.log('Hello world!'){:js}`. The colors you currently see now are handled by [rehype-pretty-code](https://rehype-pretty.pages.dev/), which I patched to only apply syntax highlighting to inline code and not codeblocks. To read more about this process, see the next blog post: [v1.3.0: "Patches in Production"](/blog/rehype-patch).
- The `cn(){:js}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
This needs to be in every single template. This is an example of it being used in my `<Link>{:html}` component:
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {10-15} "cn"
---
import { cn } from '@/lib/utils'
const { href, external, class: className, underline, ...rest } = Astro.props
---
<a
href={href}
target={external ? '_blank' : '_self'}
class={cn(
'inline-block transition-colors duration-300 ease-in-out',
underline &&
'underline decoration-muted-foreground underline-offset-[3px] hover:decoration-foreground',
className,
)}
{...rest}
>
<slot />
</a>
```
We were able to, in a single helper function:
1. Concatenate whatever the user passed via the `class{:astro}` prop to our base styles
2. Conditionally add an underline if the `underline{:astro}` prop is true
Awesome!
## Welcoming some UX features
Within the blog itself (as in the layout, appearance, and navigation) are features that I believe are essential for a great user experience:
- Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter.
- Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page).
- The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `<TableOfContents>{:html}` component which always hangs out around the unused left side margin of a blog post. I also attached a very tiny client-side script using [`IntersectionObserver{:js}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the <abbr title="Table of Contents">TOC</abbr> as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens.
- Every page, except the homepage, will have a `<Breadcrumb>{:html}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (<abbr title="Search Engine Optimization">SEO</abbr> and crawling) and user experience (the user always knows how "deep" they are in the site).
- You can specify multiple post authors via frontmatter. If this post author's ID is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered.
- Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection.
- Each tag will also have their own page, which lists all of the posts under that tag!
- $\LaTeX$ is fully supported with [KaTeX](https://katex.org/):
<blockquote>
To solve the cubic equation $t^3 + pt + q = 0$ (where the real numbers
$p, q$ satisfy ${4p^3 + 27q^2} > 0$) one can use Cardano's formula:
$$
\sqrt[{3}]{
-\frac{q}{2}
+\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
}+
\sqrt[{3}]{
-\frac{q}{2}
-\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
}
$$
For any $u_1, \dots, u_n \in \mathbb{C}$ and
$v_1, \dots, v_n \in \mathbb{C}$, the CauchyBunyakovskySchwarz
inequality can be written as follows:
$$
\left| \sum_{k=1}^n {u_k \bar{v_k}} \right|^2
\leq
{
\left( \sum_{k=1}^n {|u_k|} \right)^2
\left( \sum_{k=1}^n {|v_k|} \right)^2
}
$$
Finally, the determinant of a Vandermonde matrix can be calculated
using the following expression:
$$
\begin{vmatrix}
1 & x_1 & x_1^2 & \dots & x_1^{n-1} \\
1 & x_2 & x_2^2 & \dots & x_2^{n-1} \\
1 & x_3 & x_3^2 & \dots & x_3^{n-1} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & x_n & x_n^2 & \dots & x_n^{n-1} \\
\end{vmatrix}
= {\prod_{1 \leq {i,j} \leq n} {(x_i - x_j)}}
$$
—<cite>[Three famous mathematical formulas](https://developer.mozilla.org/en-US/docs/Learn/MathML/First_steps/Three_famous_mathematical_formulas) (Mozilla Docs)</cite>
</blockquote>
## Foregoing some slop
- Goodbye, [ESLint](https://eslint.org/)! There have been so many occasions where I've had to deal with blogging templates with in-built pre-commit hooks which enforce contrived and arbitrary linting rules that, frankly, I couldn't be bothered with. Obviously, linting is awesome for ensuring consistency and best practice, but that's for shared and large codebases. You're dealing with, at most, your MDX blog posts and some interior fetching. It's just not worth the headache.
- You probably don't need analytics via [Umami](https://umami.is) or [Plausible](https://plausible.io). Let's be realistic: for many personal blogs, unless you're an anime profile picture Twitter microcelebrity, you don't need to know how many of your readers click Big Button A versus how many click Big Button B.
- You likely don't need a comments section via [Giscus](https://giscus.app). This opens up a can of worms involving the ability to spam comments and the necessity to moderate them. If you want organic discussion about your blog posts to happen, then share on social media and let people discuss there.
- Speaking of sharing on social media, let's get rid of the share buttons. When was the last time you actually used a share button on a blog post rather than just copying the URL?
- You probably don't need a <abbr title="Content Management System">CMS</abbr> unless you have thousands of posts and/or are willing to navigate through a clunky management interface. Markdown and folders is really all you need, which you can organize to your preference via folder or file naming conventions.
- If you have literally anything involving an `.env` file in a blogging site, maybe think about what you are doing for a moment.
- Please consider not overriding the browser's <kbd>Ctrl</kbd> + <kbd>K</kbd> functionality to open up a command palette. There should not be a single reason why a user would use a small context menu to browse your blog over the `/blog` route. Most of the time, command palettes on sites do nothing more than regurgitate shortcuts that are already on the same page you're hiding with the palette's modal.
## Something important
Obviously a disclaimer: everything that I've shared here are my own personal gripes and, while I'd like for you to agree with me on a lot of these points for the better of the community, you can go ahead and disagree. The web development community, especially in spaces like Twitter and various online forums, is constantly engaged in heated debates about what constitutes "best practices." You'll find a wide spectrum of viewpoints:
1. Fundamentalists who adhere strictly to established patterns and completely disregard change,
2. Accelerationists who eat up whatever Vercel cooks as if it's the second coming of Christ,
3. and everyone in between this spectrum.
I wanted to share what particular technology stack worked the best for me in this particular use case. A stack for one project can be completely unusable for another. If you vehemently hate any of the design choices I've made then simply get rid of them. MIT license! Happy blogging.

View File

@@ -1,9 +0,0 @@
---
name: 'Project A'
description: 'This is an example project description! You should replace this with a description of your own project.'
tags: ['Framework A', 'Library B', 'Tool C', 'Resource D']
image: '../../../public/static/1200x630.png'
link: 'https://example.com'
startDate: '2024-01-01'
endDate: '2024-02-01'
---

View File

@@ -1,9 +0,0 @@
---
name: 'Project B'
description: 'This is an example project description! You should replace this with a description of your own project.'
tags: ['Framework A', 'Library B', 'Tool C', 'Resource D']
image: '../../../public/static/1200x630.png'
link: 'https://example.com'
startDate: '2024-02-01'
endDate: '2024-03-01'
---

View File

@@ -1,8 +0,0 @@
---
name: 'Project C'
description: 'This is an example project description! You should replace this with a description of your own project.'
tags: ['Framework A', 'Library B', 'Tool C', 'Resource D']
image: '../../../public/static/1200x630.png'
link: 'https://example.com'
startDate: '2024-03-01'
---

View File

@@ -0,0 +1 @@
<svg fill="none" height="51" viewBox="0 0 51 51" width="51" xmlns="http://www.w3.org/2000/svg"><path d="m40.5284 14.8238v-1.2071l-.8535.8536-29.85481 29.8548c-.86856.8685-2.27678.8685-3.14534 0-.86856-.8686-.86856-2.2768 0-3.1454l29.85475-29.8547.8536-.8536h-1.2071-17.9403c-1.2283 0-2.2241-.99576-2.2241-2.22408 0-1.22833.9958-2.22409 2.2241-2.22409h24.5168c1.2283 0 2.2241.99576 2.2241 2.22409v24.51678c0 1.2283-.9958 2.2241-2.2241 2.2241s-2.2241-.9958-2.2241-2.2241zm-22.2927.2797h7.9645l-22.80085 22.8008c-2.677512 2.6776-2.677511 7.0187 0 9.6962 2.67752 2.6775 7.01865 2.6775 9.69615 0l22.8008-22.8008v7.9644c0 3.7866 3.0696 6.8562 6.8562 6.8562s6.8562-3.0696 6.8562-6.8562v-24.51678c0-3.78658-3.0696-6.85621-6.8562-6.8562l-24.5168-.00001c-3.7865 0-6.8562 3.06963-6.8562 6.8562 0 3.78659 3.0697 6.85619 6.8562 6.85619z" fill="#fff" stroke="#fff"/><g clip-rule="evenodd" fill="#000" fill-rule="evenodd"><path d="m18.2355 5.52327h24.5168c1.5045 0 2.7241 1.21961 2.7241 2.72408v24.51675c0 1.5045-1.2196 2.7241-2.7241 2.7241-1.5044 0-2.7241-1.2196-2.7241-2.7241v-17.9402l-29.8547 29.8548c-1.06387 1.0638-2.78867 1.0638-3.85249 0s-1.06382-2.7886 0-3.8525l29.85479-29.8548h-17.9403c-1.5044 0-2.724-1.21957-2.724-2.72405 0-1.50447 1.2196-2.72408 2.724-2.72408z"/><path d="m18.2356 3.59827h24.5167c2.5677 0 4.6491 2.08146 4.6491 4.64909v24.51674c0 2.5677-2.0814 4.6491-4.6491 4.6491-2.5676 0-4.6491-2.0814-4.6491-4.6491v-13.2929l-26.5686 26.5687c-1.81555 1.8155-4.75919 1.8155-6.57477 0-1.81558-1.8156-1.81558-4.7593 0-6.5748l26.56857-26.5687h-13.2929c-2.5676 0-4.649-2.0814-4.649-4.64904 0-2.56763 2.0814-4.64909 4.6491-4.64909zm-.0001 7.37313c-1.5044 0-2.724-1.21957-2.724-2.72405 0-1.50447 1.2196-2.72408 2.724-2.72408h24.5168c1.5045 0 2.7241 1.21961 2.7241 2.72408v24.51675c0 1.5045-1.2196 2.7241-2.7241 2.7241-1.5044 0-2.7241-1.2196-2.7241-2.7241v-17.9402l-29.8547 29.8548c-1.06387 1.0638-2.78867 1.0638-3.85249 0s-1.06382-2.7886 0-3.8525l29.85479-29.8548z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -5,9 +5,9 @@ import '@/styles/typography.css'
import Footer from '@/components/Footer.astro' import Footer from '@/components/Footer.astro'
import Head from '@/components/Head.astro' import Head from '@/components/Head.astro'
import Header from '@/components/Header.astro' import Header from '@/components/Header.astro'
import { SITE } from '@/consts' import { ENABLE_LIGHTBOX, SITE } from '@/consts'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { getStaticFilePath } from '@/lib/blog-helpers'
interface Props { interface Props {
class?: string class?: string
} }
@@ -37,5 +37,12 @@ const { class: className } = Astro.props
<slot /> <slot />
</main> </main>
<Footer /> <Footer />
{
ENABLE_LIGHTBOX && (
<script src={getStaticFilePath('/js/fslightbox.js')} />
)
}
</body> </body>
</html> </html>

180
src/lib/blog-helpers.ts Normal file
View File

@@ -0,0 +1,180 @@
import type {
Heading1,
Heading2,
Heading3,
RichText,
Text,
} from './interfaces'
function toSlug(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
}
function extractHeadingText(
heading?: Heading1 | Heading2 | Heading3,
): { text: string; slug: string } {
const parts = heading?.RichTexts ?? []
const plain = parts.map((r) => r.PlainText || r.Text?.Content || '').join('')
const slug = toSlug(plain) || heading?.RichTexts?.[0]?.PlainText || ''
return { text: plain, slug: slug || 'heading' }
}
export function buildHeadingId(
heading?: Heading1 | Heading2 | Heading3,
): string {
const { slug } = extractHeadingText(heading)
return slug || 'heading'
}
export function isAmazonURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /amazon\.[a-z.]+|amzn\.to/.test(href)
}
export function isGitHubURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /github\.com/.test(href)
}
export function isTweetURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /twitter\.com|x\.com/.test(href)
}
export function isTikTokURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /tiktok\.com/.test(href)
}
export function isInstagramURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /instagram\.com/.test(href)
}
export function isPinterestURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /pinterest\.com|pin\.it/.test(href)
}
export function isCodePenURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /codepen\.io/.test(href)
}
export function isCircuitSimulatorAppletURL(
url?: URL | string | null,
): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /falstad\.com\/circuit/.test(href)
}
export function isYouTubeURL(url?: URL | string | null): boolean {
if (!url) return false
const href = typeof url === 'string' ? url : url.toString()
return /youtube\.com|youtu\.be/.test(href)
}
export function parseYouTubeVideoId(url?: URL | string | null): string {
if (!url) return ''
const target = typeof url === 'string' ? new URL(url) : url
if (target.hostname.includes('youtu.be')) {
return target.pathname.replace('/', '')
}
if (target.searchParams.has('v')) {
return target.searchParams.get('v') || ''
}
const parts = target.pathname.split('/')
return parts.pop() || ''
}
export function filePath(url: URL | string): string {
return typeof url === 'string' ? url : url.toString()
}
export function getPostLink(slug: string): string {
return `/blog/${slug}`
}
export function richTextToPlainText(richTexts: RichText[] = []): string {
return richTexts
.map((r) => r.PlainText || r.Text?.Content || '')
.filter(Boolean)
.join('')
}
export function getStaticFilePath(path: string): string {
return `${path}`
}
export function buildText(
content: string,
link?: string,
annotation?: Partial<RichText['Annotation']>,
): RichText {
const text: Text = { Content: content }
if (link) {
text.Link = { Url: link }
}
return {
Text: text,
Annotation: {
Bold: false,
Italic: false,
Strikethrough: false,
Underline: false,
Code: false,
Color: 'default',
...(annotation || {}),
},
PlainText: content,
Href: link,
}
}
export function toNotionImageUrl(
url: string,
block?: { parent_table?: string; id?: string },
): string {
if (!url) return url
let notionUrl = url
if (!url.startsWith('https://www.notion.so')) {
notionUrl = 'https://www.notion.so'.concat(
url.startsWith('/image') ? url : `/image/${encodeURIComponent(url)}`,
)
}
try {
const imageUrl = new URL(notionUrl)
if (block) {
const table = ['space', 'team'].includes(block.parent_table || '')
? 'block'
: block.parent_table
if (table) {
imageUrl.searchParams.set('table', table)
}
if (block.id) {
imageUrl.searchParams.set('id', block.id)
}
imageUrl.searchParams.set('cache', 'v2')
}
return imageUrl.toString()
} catch (err) {
console.error('toNotionImageUrl error:', err)
return url
}
}

View File

@@ -1,5 +1,14 @@
import { getCollection, render, type CollectionEntry } from 'astro:content' import { getCollection, render, type CollectionEntry } from 'astro:content'
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils' import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
import type { Block, RichText } from './interfaces'
import {
buildHeadingId,
buildText,
richTextToPlainText,
toNotionImageUrl,
} from './blog-helpers'
const DEFAULT_AUTHOR_ID = 'libr'
type NotionPost = { type NotionPost = {
id: string id: string
@@ -13,6 +22,28 @@ type NotionPost = {
last_edited_time?: string last_edited_time?: string
} }
export type NotionBlockValue = {
id: string
type: string
properties?: Record<string, any>
content?: string[]
format?: Record<string, any>
synced_from?: Record<string, any>
link_to_page_id?: string
parent_table?: string
}
export type NotionBlock = {
value: NotionBlockValue
}
export type NotionBlockMap = Record<string, NotionBlock>
export type RemotePostPayload = {
post: NotionPost
blockMap: NotionBlockMap
}
export interface LinkEntry { export interface LinkEntry {
id: string id: string
picLink?: string picLink?: string
@@ -36,6 +67,14 @@ export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
return posts return posts
.filter((post) => !post.data.draft && !isSubpost(post.id)) .filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.map((post) => ({
...post,
data: {
...post.data,
banner: post.data.banner ?? post.data.image,
authors: [DEFAULT_AUTHOR_ID],
},
}))
} }
try { try {
@@ -51,6 +90,7 @@ export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
post['Published Date'] ?? post.created_time ?? new Date().toISOString() post['Published Date'] ?? post.created_time ?? new Date().toISOString()
const date = new Date(dateString) const date = new Date(dateString)
const id = post.slug || post.id const id = post.slug || post.id
const banner = (post as any).banner || null
return { return {
id, id,
@@ -61,7 +101,8 @@ export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
date, date,
tags: Array.isArray(post.Tags) ? post.Tags : [], tags: Array.isArray(post.Tags) ? post.Tags : [],
draft: !(post.Published ?? true), draft: !(post.Published ?? true),
authors: [], authors: [DEFAULT_AUTHOR_ID],
banner,
}, },
body: '', body: '',
} }
@@ -256,6 +297,21 @@ export async function fetchRemotePost(
} }
} }
export async function fetchRemotePostContent(
slug: string,
): Promise<RemotePostPayload | null> {
try {
const res = await fetch(`${POSTS_API_URL}/${encodeURI(slug)}`)
if (!res.ok) throw new Error(`Failed to fetch post content: ${res.status}`)
const data = (await res.json()) as Partial<RemotePostPayload>
if (!data.post || !data.blockMap) return null
return data as RemotePostPayload
} catch (error) {
console.error(`fetchRemotePostContent error for slug "${slug}":`, error)
return null
}
}
export async function getFriendLinks(): Promise<LinkEntry[]> { export async function getFriendLinks(): Promise<LinkEntry[]> {
const fallback: LinkEntry[] = [] const fallback: LinkEntry[] = []
@@ -345,6 +401,579 @@ export type TOCHeading = {
isSubpostTitle?: boolean isSubpostTitle?: boolean
} }
export type RenderedRemoteContent = {
blocks: Block[]
headingBlocks: Block[]
headings: TOCHeading[]
wordCount: number
}
function parseRichTexts(raw: any): RichText[] {
if (!Array.isArray(raw)) return []
return raw.map((segment: any) => {
const [text, decorations] = segment
const annotation: RichText['Annotation'] = {
Bold: false,
Italic: false,
Strikethrough: false,
Underline: false,
Code: false,
Color: 'default',
}
let href: string | undefined
let mentionPageId: string | undefined
if (Array.isArray(decorations)) {
for (const deco of decorations) {
const [type, value] = deco
switch (type) {
case 'b':
annotation.Bold = true
break
case 'i':
annotation.Italic = true
break
case 's':
annotation.Strikethrough = true
break
case '_':
annotation.Underline = true
break
case 'c':
annotation.Code = true
break
case 'a':
href =
typeof value === 'string'
? value
: typeof value?.[0] === 'string'
? value[0]
: undefined
break
case 'p':
mentionPageId =
typeof value === 'object' && value?.id ? value.id : undefined
break
case 'h':
if (typeof value === 'string') {
annotation.Color = value
}
break
default:
break
}
}
}
const richText = buildText(typeof text === 'string' ? text : '', href, {
Color: annotation.Color,
Bold: annotation.Bold,
Italic: annotation.Italic,
Strikethrough: annotation.Strikethrough,
Underline: annotation.Underline,
Code: annotation.Code,
})
if (mentionPageId) {
richText.Mention = {
Type: 'page',
Page: { Id: mentionPageId },
}
}
return richText
})
}
function countWordsFromRichTexts(richTexts: RichText[] = []): number {
const text = richTextToPlainText(richTexts)
if (!text) return 0
return text.split(/\s+/).filter(Boolean).length
}
function countWords(blocks: Block[]): number {
let total = 0
const addChildren = (children?: Block[]) => {
if (children && children.length > 0) {
total += countWords(children)
}
}
for (const block of blocks) {
switch (block.Type) {
case 'paragraph':
total += countWordsFromRichTexts(block.Paragraph?.RichTexts)
addChildren(block.Paragraph?.Children)
break
case 'heading_1':
total += countWordsFromRichTexts(block.Heading1?.RichTexts)
addChildren(block.Heading1?.Children)
break
case 'heading_2':
total += countWordsFromRichTexts(block.Heading2?.RichTexts)
addChildren(block.Heading2?.Children)
break
case 'heading_3':
total += countWordsFromRichTexts(block.Heading3?.RichTexts)
addChildren(block.Heading3?.Children)
break
case 'bulleted_list':
case 'numbered_list':
block.ListItems?.forEach((item) => {
if (item.Type === 'bulleted_list_item') {
total += countWordsFromRichTexts(item.BulletedListItem?.RichTexts)
addChildren(item.BulletedListItem?.Children)
} else if (item.Type === 'numbered_list_item') {
total += countWordsFromRichTexts(item.NumberedListItem?.RichTexts)
addChildren(item.NumberedListItem?.Children)
}
})
break
case 'to_do':
total += countWordsFromRichTexts(block.ToDo?.RichTexts)
addChildren(block.ToDo?.Children)
break
case 'quote':
total += countWordsFromRichTexts(block.Quote?.RichTexts)
addChildren(block.Quote?.Children)
break
case 'callout':
total += countWordsFromRichTexts(block.Callout?.RichTexts)
addChildren(block.Callout?.Children)
break
case 'code':
total += countWordsFromRichTexts(block.Code?.RichTexts)
break
case 'image':
total += countWordsFromRichTexts(block.Image?.Caption)
break
case 'bookmark':
total += countWordsFromRichTexts(block.Bookmark?.Caption)
break
case 'link_preview':
break
case 'embed':
break
case 'video':
total += countWordsFromRichTexts(block.Video?.Caption)
break
case 'column_list':
block.ColumnList?.Columns?.forEach((column) => {
addChildren(column.Children)
})
break
case 'synced_block':
addChildren(block.SyncedBlock?.Children)
break
case 'toggle':
total += countWordsFromRichTexts(block.Toggle?.RichTexts)
addChildren(block.Toggle?.Children)
break
default:
break
}
}
return total
}
function buildBlocks(
contentIds: string[],
blockMap: NotionBlockMap,
headingBlocks: Block[],
): Block[] {
const blocks: Block[] = []
let i = 0
while (i < contentIds.length) {
const currentId = contentIds[i]
const current = blockMap[currentId]?.value
if (!current) {
i++
continue
}
if (current.type === 'bulleted_list' || current.type === 'numbered_list') {
const listItems: Block[] = []
const targetType = current.type
while (
i < contentIds.length &&
blockMap[contentIds[i]]?.value?.type === targetType
) {
const item = blockMap[contentIds[i]]?.value
const childIds = Array.isArray(item?.content) ? item.content : []
const children = buildBlocks(childIds, blockMap, headingBlocks)
if (targetType === 'bulleted_list') {
listItems.push({
Id: item?.id ?? contentIds[i],
Type: 'bulleted_list_item',
HasChildren: children.length > 0,
BulletedListItem: {
RichTexts: parseRichTexts(item?.properties?.title),
Color:
typeof item?.format?.block_color === 'string'
? item?.format?.block_color
: 'default',
Children: children.length > 0 ? children : undefined,
},
})
} else {
listItems.push({
Id: item?.id ?? contentIds[i],
Type: 'numbered_list_item',
HasChildren: children.length > 0,
NumberedListItem: {
RichTexts: parseRichTexts(item?.properties?.title),
Color:
typeof item?.format?.block_color === 'string'
? item?.format?.block_color
: 'default',
Children: children.length > 0 ? children : undefined,
},
})
}
i++
}
blocks.push({
Id: `${targetType}-${listItems[0]?.Id ?? i}`,
Type: targetType === 'bulleted_list' ? 'bulleted_list' : 'numbered_list',
HasChildren: listItems.some((item) => item.HasChildren),
ListItems: listItems,
})
continue
}
const block = convertBlock(currentId, blockMap, headingBlocks)
if (block) {
blocks.push(block)
}
i++
}
return blocks
}
function convertBlock(
blockId: string,
blockMap: NotionBlockMap,
headingBlocks: Block[],
): Block | null {
const block = blockMap[blockId]?.value
if (!block) return null
const childIds = Array.isArray(block.content) ? block.content : []
const children =
childIds.length > 0 ? buildBlocks(childIds, blockMap, headingBlocks) : []
const color =
typeof block.format?.block_color === 'string'
? block.format?.block_color
: 'default'
switch (block.type) {
case 'text': {
return {
Id: block.id,
Type: 'paragraph',
HasChildren: children.length > 0,
Paragraph: {
RichTexts: parseRichTexts(block.properties?.title),
Color: color,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'header':
case 'sub_header':
case 'sub_sub_header': {
const heading = {
RichTexts: parseRichTexts(block.properties?.title),
Color: color,
IsToggleable: false,
Children: children.length > 0 ? children : undefined,
}
const headingBlock: Block =
block.type === 'header'
? {
Id: block.id,
Type: 'heading_1',
HasChildren: children.length > 0,
Heading1: heading,
}
: block.type === 'sub_header'
? {
Id: block.id,
Type: 'heading_2',
HasChildren: children.length > 0,
Heading2: heading,
}
: {
Id: block.id,
Type: 'heading_3',
HasChildren: children.length > 0,
Heading3: heading,
}
headingBlocks.push(headingBlock)
return headingBlock
}
case 'code': {
return {
Id: block.id,
Type: 'code',
HasChildren: false,
Code: {
Caption: parseRichTexts(block.properties?.caption),
RichTexts: parseRichTexts(block.properties?.title),
Language:
block.properties?.language?.[0]?.[0]?.toString()?.toLowerCase() ||
'plain text',
},
}
}
case 'image': {
const src =
block.properties?.source?.[0]?.[0] || block.format?.display_source
if (!src) return null
const processed = toNotionImageUrl(src, block)
return {
Id: block.id,
Type: 'image',
HasChildren: false,
Image: {
Caption: parseRichTexts(block.properties?.caption),
Type: 'external',
External: { Url: processed },
Width: block.format?.block_width,
Height: block.format?.block_height,
},
}
}
case 'divider': {
return { Id: block.id, Type: 'divider', HasChildren: false }
}
case 'quote': {
return {
Id: block.id,
Type: 'quote',
HasChildren: children.length > 0,
Quote: {
RichTexts: parseRichTexts(block.properties?.title),
Color: color,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'callout': {
return {
Id: block.id,
Type: 'callout',
HasChildren: children.length > 0,
Callout: {
RichTexts: parseRichTexts(block.properties?.title),
Icon: block.format?.page_icon
? { Type: 'emoji', Emoji: block.format?.page_icon }
: null,
Color: color,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'tweet': {
const url = block.properties?.source?.[0]?.[0] || ''
if (!url) return null
return {
Id: block.id,
Type: 'embed',
HasChildren: false,
Embed: { Url: url },
}
}
case 'bookmark': {
const url =
block.properties?.link_url?.[0]?.[0] ||
block.properties?.source?.[0]?.[0] ||
block.properties?.title?.[0]?.[0] ||
''
if (!url) return null
return {
Id: block.id,
Type: 'bookmark',
HasChildren: false,
Bookmark: {
Caption: parseRichTexts(block.properties?.caption),
Url: url,
},
}
}
case 'link_preview': {
const url =
block.properties?.link_url?.[0]?.[0] ||
block.properties?.source?.[0]?.[0]
if (!url) return null
return {
Id: block.id,
Type: 'link_preview',
HasChildren: false,
LinkPreview: { Url: url },
}
}
case 'embed': {
const url =
block.properties?.source?.[0]?.[0] || block.format?.display_source
if (!url) return null
return {
Id: block.id,
Type: 'embed',
HasChildren: false,
Embed: { Url: url },
}
}
case 'video': {
const url =
block.properties?.source?.[0]?.[0] || block.format?.display_source
if (!url) return null
return {
Id: block.id,
Type: 'video',
HasChildren: false,
Video: {
Caption: parseRichTexts(block.properties?.caption),
Type: 'external',
External: { Url: url },
},
}
}
case 'to_do': {
const checked =
block.properties?.checked?.[0]?.[0]?.toString().toLowerCase() === 'yes'
return {
Id: block.id,
Type: 'to_do',
HasChildren: children.length > 0,
ToDo: {
RichTexts: parseRichTexts(block.properties?.title),
Checked: checked,
Color: color,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'toggle': {
return {
Id: block.id,
Type: 'toggle',
HasChildren: children.length > 0,
Toggle: {
RichTexts: parseRichTexts(block.properties?.title),
Color: color,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'column_list': {
const columns =
childIds.map((id) => {
const col = blockMap[id]?.value
const colChildren = Array.isArray(col?.content)
? buildBlocks(col?.content, blockMap, headingBlocks)
: []
return {
Id: id,
Type: 'column',
HasChildren: colChildren.length > 0,
Children: colChildren,
}
}) || []
return {
Id: block.id,
Type: 'column_list',
HasChildren: columns.some((c) => c.HasChildren),
ColumnList: { Columns: columns },
}
}
case 'synced_block': {
const syncedFrom =
block.synced_from && typeof block.synced_from.block_id === 'string'
? { BlockId: block.synced_from.block_id }
: null
return {
Id: block.id,
Type: 'synced_block',
HasChildren: children.length > 0,
SyncedBlock: {
SyncedFrom: syncedFrom,
Children: children.length > 0 ? children : undefined,
},
}
}
case 'table_of_contents': {
return {
Id: block.id,
Type: 'table_of_contents',
HasChildren: false,
TableOfContents: { Color: color },
}
}
case 'link_to_page': {
const pageId =
block.link_to_page_id || block.properties?.link_to_page_id?.[0]?.[0]
return {
Id: block.id,
Type: 'link_to_page',
HasChildren: false,
LinkToPage: {
Type: 'page',
PageId: pageId,
},
}
}
default:
return null
}
}
export function renderRemoteBlockMap(
blockMap: NotionBlockMap,
rootId: string,
): RenderedRemoteContent {
const headingBlocks: Block[] = []
const root = blockMap[rootId]?.value
const contentOrder = Array.isArray(root?.content)
? root?.content
: Object.keys(blockMap)
const blocks = buildBlocks(contentOrder, blockMap, headingBlocks)
const headings: TOCHeading[] = headingBlocks.map((block) => {
const heading = block.Heading1 || block.Heading2 || block.Heading3
return {
slug: buildHeadingId(heading),
text: richTextToPlainText(heading?.RichTexts ?? []),
depth:
block.Type === 'heading_1'
? 2
: block.Type === 'heading_2'
? 3
: 4,
}
})
const wordCount = countWords(blocks)
return { blocks, headingBlocks, headings, wordCount }
}
export type TOCSection = { export type TOCSection = {
type: 'parent' | 'subpost' type: 'parent' | 'subpost'
title: string title: string

269
src/lib/interfaces.ts Normal file
View File

@@ -0,0 +1,269 @@
export interface Database {
Title: string
Description: string
Icon: FileObject | Emoji | null
Cover: FileObject | null
}
export interface Post {
PageId: string
Title: string
Icon: FileObject | Emoji | null
Cover: FileObject | null
Slug: string
Date: string
Tags: SelectProperty[]
Excerpt: string
FeaturedImage: FileObject | null
Rank: number
}
export interface Block {
Id: string
Type: string
HasChildren: boolean
ListItems?: Block[]
Paragraph?: Paragraph
Heading1?: Heading1
Heading2?: Heading2
Heading3?: Heading3
BulletedListItem?: BulletedListItem
NumberedListItem?: NumberedListItem
ToDo?: ToDo
Image?: Image
File?: File
Code?: Code
Quote?: Quote
Equation?: Equation
Callout?: Callout
SyncedBlock?: SyncedBlock
Toggle?: Toggle
Embed?: Embed
Video?: Video
Bookmark?: Bookmark
LinkPreview?: LinkPreview
Table?: Table
ColumnList?: ColumnList
TableOfContents?: TableOfContents
LinkToPage?: LinkToPage
}
export interface Paragraph {
RichTexts: RichText[]
Color: string
Children?: Block[]
}
export interface Heading1 {
RichTexts: RichText[]
Color: string
IsToggleable: boolean
Children?: Block[]
}
export interface Heading2 {
RichTexts: RichText[]
Color: string
IsToggleable: boolean
Children?: Block[]
}
export interface Heading3 {
RichTexts: RichText[]
Color: string
IsToggleable: boolean
Children?: Block[]
}
export interface BulletedListItem {
RichTexts: RichText[]
Color: string
Children?: Block[]
}
export interface NumberedListItem {
RichTexts: RichText[]
Color: string
Children?: Block[]
}
export interface ToDo {
RichTexts: RichText[]
Checked: boolean
Color: string
Children?: Block[]
}
export interface Image {
Caption: RichText[]
Type: string
File?: FileObject
External?: External
Width?: number
Height?: number
}
export interface Video {
Caption: RichText[]
Type: string
External?: External
}
export interface File {
Caption: RichText[]
Type: string
File?: FileObject
External?: External
}
export interface FileObject {
Type: string
Url: string
ExpiryTime?: string
}
export interface External {
Url: string
}
export interface Code {
Caption: RichText[]
RichTexts: RichText[]
Language: string
}
export interface Quote {
RichTexts: RichText[]
Color: string
Children?: Block[]
}
export interface Equation {
Expression: string
}
export interface Callout {
RichTexts: RichText[]
Icon: FileObject | Emoji | null
Color: string
Children?: Block[]
}
export interface SyncedBlock {
SyncedFrom: SyncedFrom | null
Children?: Block[]
}
export interface SyncedFrom {
BlockId: string
}
export interface Toggle {
RichTexts: RichText[]
Color: string
Children?: Block[]
}
export interface Embed {
Url: string
}
export interface Bookmark {
Caption: RichText[]
Url: string
}
export interface LinkPreview {
Url: string
}
export interface Table {
TableWidth: number
HasColumnHeader: boolean
HasRowHeader: boolean
Rows: TableRow[]
}
export interface TableRow {
Id: string
Type: string
HasChildren: boolean
Cells: TableCell[]
}
export interface TableCell {
RichTexts: RichText[]
}
export interface ColumnList {
Columns: Column[]
}
export interface Column {
Id: string
Type: string
HasChildren: boolean
Children: Block[]
}
export interface List {
Type: string
ListItems: Block[]
}
export interface TableOfContents {
Color: string
}
export interface RichText {
Text?: Text
Annotation: Annotation
PlainText: string
Href?: string
Equation?: Equation
Mention?: Mention
}
export interface Text {
Content: string
Link?: Link
}
export interface Emoji {
Type: string
Emoji: string
}
export interface Annotation {
Bold: boolean
Italic: boolean
Strikethrough: boolean
Underline: boolean
Code: boolean
Color: string
}
export interface Link {
Url: string
}
export interface SelectProperty {
id: string
name: string
color: string
}
export interface LinkToPage {
Type: string
PageId: string
}
export interface Mention {
Type: string
Page?: Reference
}
export interface Reference {
Id: string
}

40
src/lib/notion/client.ts Normal file
View File

@@ -0,0 +1,40 @@
import { POSTS_API_URL } from '../data-utils'
import type { Post } from '../interfaces'
export async function getPostByPageId(pageId: string): Promise<Post | null> {
if (!pageId) return null
try {
const res = await fetch(POSTS_API_URL)
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
const payload = (await res.json()) as { posts?: any[] }
const match = (payload.posts ?? []).find((post) => post.id === pageId)
if (!match) return null
const dateString =
match['Published Date'] || match.created_time || new Date().toISOString()
return {
PageId: match.id,
Title: match.Content || match.slug || 'Untitled',
Icon: null,
Cover: null,
Slug: match.slug || match.id,
Date: dateString,
Tags: Array.isArray(match.Tags)
? match.Tags.map((name: string) => ({
id: name,
name,
color: 'default',
}))
: [],
Excerpt: match.excerpt || '',
FeaturedImage: null,
Rank: Number(match.rank || 0),
}
} catch (error) {
console.error('getPostByPageId error:', error)
return null
}
}

4
src/lib/style-helpers.ts Normal file
View File

@@ -0,0 +1,4 @@
export function snakeToKebab(text: string | undefined): string {
if (!text) return ''
return text.replace(/_/g, '-')
}

View File

@@ -22,7 +22,7 @@ export function calculateWordCountFromHtml(
} }
export function readingTime(wordCount: number): string { export function readingTime(wordCount: number): string {
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200)) const readingTimeMinutes = Math.max(1, Math.round(wordCount / 100))
return `${readingTimeMinutes} min read` return `${readingTimeMinutes} min read`
} }

View File

@@ -7,6 +7,7 @@ import SubpostsHeader from '@/components/SubpostsHeader.astro'
import SubpostsSidebar from '@/components/SubpostsSidebar.astro' import SubpostsSidebar from '@/components/SubpostsSidebar.astro'
import TOCHeader from '@/components/TOCHeader.astro' import TOCHeader from '@/components/TOCHeader.astro'
import TOCSidebar from '@/components/TOCSidebar.astro' import TOCSidebar from '@/components/TOCSidebar.astro'
import NotionBlocks from '@/components/NotionBlocks.astro'
import { badgeVariants } from '@/components/ui/badge' import { badgeVariants } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import Layout from '@/layouts/Layout.astro' import Layout from '@/layouts/Layout.astro'
@@ -19,11 +20,14 @@ import {
getPostReadingTime, getPostReadingTime,
getSubpostCount, getSubpostCount,
getTOCSections, getTOCSections,
fetchRemotePostContent,
renderRemoteBlockMap,
hasSubposts, hasSubposts,
isSubpost, isSubpost,
parseAuthors, parseAuthors,
} from '@/lib/data-utils' } from '@/lib/data-utils'
import { formatDate } from '@/lib/utils' import type { TOCSection } from '@/lib/data-utils'
import { formatDate, readingTime } from '@/lib/utils'
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import { render } from 'astro:content' import { render } from 'astro:content'
@@ -38,7 +42,22 @@ export async function getStaticPaths() {
const post = Astro.props const post = Astro.props
const currentPostId = Astro.params.id const currentPostId = Astro.params.id
const { Content, headings } = await render(post) const isRemotePost = post.body === ''
let Content: any = null
let headings
let remoteContent = null
if (isRemotePost) {
const remote = await fetchRemotePostContent(currentPostId)
remoteContent = remote ? renderRemoteBlockMap(remote.blockMap, remote.post.id) : null
headings = remoteContent?.headings ?? []
} else {
const rendered = await render(post)
Content = rendered.Content
headings = rendered.headings
}
const authors = await parseAuthors(post.data.authors ?? []) const authors = await parseAuthors(post.data.authors ?? [])
const isCurrentSubpost = isSubpost(currentPostId) const isCurrentSubpost = isSubpost(currentPostId)
@@ -49,13 +68,31 @@ const hasChildPosts = await hasSubposts(currentPostId)
const subpostCount = !isCurrentSubpost const subpostCount = !isCurrentSubpost
? await getSubpostCount(currentPostId) ? await getSubpostCount(currentPostId)
: 0 : 0
const postReadingTime = await getPostReadingTime(currentPostId) const postReadingTime = remoteContent
? readingTime(remoteContent.wordCount)
: await getPostReadingTime(currentPostId)
const combinedReadingTime = const combinedReadingTime =
hasChildPosts && !isCurrentSubpost !remoteContent && hasChildPosts && !isCurrentSubpost
? await getCombinedReadingTime(currentPostId) ? await getCombinedReadingTime(currentPostId)
: null : null
const tocSections = await getTOCSections(currentPostId) const tocSections: TOCSection[] = remoteContent
? remoteContent.headings.length > 0
? [
{
type: 'parent',
title: 'Overview',
headings: remoteContent.headings,
},
]
: []
: await getTOCSections(currentPostId)
const heroImage =
post.data.banner && typeof post.data.banner === 'object' && 'src' in post.data.banner
? post.data.banner
: post.data.image && typeof post.data.image === 'object' && 'src' in post.data.image
? post.data.image
: null
--- ---
<Layout> <Layout>
@@ -109,9 +146,9 @@ const tocSections = await getTOCSections(currentPostId)
</div> </div>
{ {
post.data.image && ( heroImage && (
<Image <Image
src={post.data.image} src={heroImage}
alt={post.data.title} alt={post.data.title}
width={1200} width={1200}
height={630} height={630}
@@ -224,9 +261,46 @@ const tocSections = await getTOCSections(currentPostId)
} }
<article class="prose col-start-2 max-w-none"> <article class="prose col-start-2 max-w-none">
<Content /> {
remoteContent ? (
remoteContent.blocks.length > 0 ? (
<NotionBlocks
blocks={remoteContent.blocks}
headings={remoteContent.headingBlocks}
/>
) : (
<p>Content unavailable.</p>
)
) : (
Content && <Content />
)
}
</article> </article>
<div class="col-start-2">
<div id="tcomment"></div>
</div>
<script
is:inline
src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js"
></script>
<script is:inline>
const mountTwikoo = () => {
if (!window.twikoo) return
window.twikoo.init({
envId: 'https://twikoo.hk.nvme0n1p.dev/',
el: '#tcomment',
})
}
if (document.readyState === 'complete') {
mountTwikoo()
} else {
addEventListener('astro:page-load', mountTwikoo)
addEventListener('DOMContentLoaded', mountTwikoo, { once: true })
}
</script>
{ {
(hasChildPosts || isCurrentSubpost) && ( (hasChildPosts || isCurrentSubpost) && (
<SubpostsSidebar <SubpostsSidebar

104
src/styles/notion-color.css Normal file
View File

@@ -0,0 +1,104 @@
.gray,
.gray:hover {
color: rgba(120, 119, 116, 1);
}
.brown,
.brown:hover {
color: rgba(159, 107, 83, 1);
}
.orange,
.orange:hover {
color: rgba(217, 115, 13, 1);
}
.yellow,
.yellow:hover {
color: rgba(203, 145, 47, 1);
}
.green,
.green:hover {
color: rgba(68, 131, 97, 1);
}
.blue,
.blue:hover {
color: rgba(51, 126, 169, 1);
}
.purple,
.purple:hover {
color: rgba(144, 101, 176, 1);
}
.pink,
.pink:hover {
color: rgba(193, 76, 138, 1);
}
.red,
.red:hover {
color: rgba(212, 76, 71, 1);
}
.gray-background {
background: rgba(241, 241, 239, 1) !important;
}
.brown-background {
background: rgba(244, 238, 238, 1) !important;
}
.orange-background {
background: rgba(251, 236, 221, 1) !important;
}
.yellow-background {
background: rgba(251, 243, 219, 1) !important;
}
.green-background {
background: rgba(237, 243, 236, 1) !important;
}
.blue-background {
background: rgba(231, 243, 248, 1) !important;
}
.purple-background {
background: rgba(244, 240, 247, 0.8) !important;
}
.pink-background {
background: rgba(249, 238, 243, 0.8) !important;
}
.red-background {
background: rgba(253, 235, 236, 1) !important;
}
.tag.light-gray {
color: rgb(28, 56, 41);
background: rgba(227, 226, 224, 0.5) !important;
}
.tag.gray {
color: rgb(28, 56, 41);
background: rgb(227, 226, 224) !important;
}
.tag.brown {
color: rgb(28, 56, 41);
background: rgb(238, 224, 218) !important;
}
.tag.orange {
color: rgb(28, 56, 41);
background: rgb(250, 222, 201) !important;
}
.tag.yellow {
color: rgb(28, 56, 41);
background: rgb(253, 236, 200) !important;
}
.tag.green {
color: rgb(28, 56, 41);
background: rgb(219, 237, 219) !important;
}
.tag.blue {
color: rgb(28, 56, 41);
background: rgb(211, 229, 239) !important;
}
.tag.purple {
color: rgb(28, 56, 41);
background: rgb(232, 222, 238) !important;
}
.tag.pink {
color: rgb(28, 56, 41);
background: rgb(245, 224, 233) !important;
}
.tag.red {
color: rgb(28, 56, 41);
background: rgb(255, 226, 221) !important;
}

View File

@@ -0,0 +1,70 @@
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.namespace {
opacity: 0.7;
}
.token.string,
.token.attr-value {
color: #690;
}
.token.punctuation {
color: #999;
}
.token.operator {
color: #9a6e3a;
}
.token.entity,
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.property,
.token.regex {
color: #905;
}
.token.prefix.inserted {
color: #690;
}
.token.prefix.deleted {
color: #dd4a68;
}
.token.atrule,
.token.keyword,
.token.attr-name,
.language-autohotkey .token.selector {
color: #07a;
}
.token.function,
.language-autohotkey .token.tag {
color: #dd4a68;
}
.token.tag,
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}

1
src/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'prismjs'