From e7515880e4b409a27e03798238f251846c0f0186 Mon Sep 17 00:00:00 2001 From: Yifei Gao Date: Thu, 12 Sep 2024 18:48:08 +0800 Subject: [PATCH 1/4] feat: keep the dom elements in code snippet --- gatsby/lib/codesplit.mjs | 9 +- package-lock.json | 362 +++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 368 insertions(+), 4 deletions(-) diff --git a/gatsby/lib/codesplit.mjs b/gatsby/lib/codesplit.mjs index f8a48aa..d667ca0 100644 --- a/gatsby/lib/codesplit.mjs +++ b/gatsby/lib/codesplit.mjs @@ -1,6 +1,7 @@ import { h } from 'hastscript'; import { visit } from 'unist-util-visit'; -import { toString } from 'hast-util-to-string'; +import { toHtml } from 'hast-util-to-html'; +import { fromHtml } from 'hast-util-from-html'; /** * Modifed from https://github.com/magicbookproject/magicbook-codesplit/blob/master/src/codesplit.js @@ -21,7 +22,7 @@ export const rehypeCodesplit = () => (tree) => { node.properties.className && node.properties.className.includes('codesplit') ) { - const lines = toString(node).split('\n'); + const lines = toHtml(node.children).split('\n'); const lang = node.properties.dataCodeLanguage; // if the first line was a linebreak, let's get rid of it. @@ -101,7 +102,7 @@ export const rehypeCodesplit = () => (tree) => { pairs.push(pair); const children = pairs.map((pair) => { - const code = pair.code.join('\n') + '\n'; + const code = fromHtml(pair.code.join('\n') + '\n', { fragment: true }); const comments = pair.comment .map((str) => str.replace('//', '').trim()) .filter((str) => !!str); @@ -113,7 +114,7 @@ export const rehypeCodesplit = () => (tree) => { return h('div', { className }, [ h('pre', [h('code', { class: ['code', `language-${lang}`] }, code)]), - h('div', { class: ['comment'] }, h('p', comments.join('\n'))), + h('div', { class: ['comment'] }, h('p', fromHtml(comments.join('\n'), { fragment: true }))), ]); }); diff --git a/package-lock.json b/package-lock.json index ec2ad61..5dbbdb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "gatsby-transformer-json": "^4.19.0", "gatsby-transformer-sharp": "^4.19.0", "glob": "^10.3.9", + "hast-util-to-html": "^9.0.2", "jest": "^29.7.0", "katex": "^0.16.9", "lodash-es": "^4.17.21", @@ -5284,6 +5285,15 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -8000,6 +8010,16 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -8057,6 +8077,26 @@ "node": ">=10" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -14729,6 +14769,57 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.2.tgz", + "integrity": "sha512-RP5wNpj5nm1Z8cloDv4Sl4RS8jH5HYa0v93YB6Wb4poEzgMo/dAAL0KcT4974dCjcNG5pkLqTImeFHHCwwfY3g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-to-html/node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", @@ -14861,6 +14952,16 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlescape": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", @@ -18705,6 +18806,125 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -18803,6 +19023,95 @@ "node": ">= 0.6" } }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -24120,6 +24429,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -25081,6 +25404,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -25465,6 +25798,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/unist-util-remove": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-3.1.1.tgz", @@ -26721,6 +27073,16 @@ "engines": { "node": ">=6" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index c6bf357..57dd242 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "gatsby-transformer-json": "^4.19.0", "gatsby-transformer-sharp": "^4.19.0", "glob": "^10.3.9", + "hast-util-to-html": "^9.0.2", "jest": "^29.7.0", "katex": "^0.16.9", "lodash-es": "^4.17.21", From f5d214784abe90daa64c69193a5f806d41f23c48 Mon Sep 17 00:00:00 2001 From: Yifei Gao Date: Thu, 12 Sep 2024 23:48:50 +0800 Subject: [PATCH 2/4] feat: implement the fill-in-blank feature and more - reveal / hide answer - copy raw code --- gatsby/lib/blank-span.mjs | 73 +++++++++++++++++++++++++++++++ gatsby/lib/codesplit.mjs | 11 ++++- gatsby/lib/parse-content.mjs | 3 ++ src/components/Codesplit.js | 84 ++++++++++++++++++++++++++++++++++++ src/layouts/ChapterLayout.js | 2 + src/styles/codesplit.css | 14 ++++-- 6 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 gatsby/lib/blank-span.mjs create mode 100644 src/components/Codesplit.js diff --git a/gatsby/lib/blank-span.mjs b/gatsby/lib/blank-span.mjs new file mode 100644 index 0000000..ceb5a0c --- /dev/null +++ b/gatsby/lib/blank-span.mjs @@ -0,0 +1,73 @@ +import { toString } from 'hast-util-to-string'; +import { visit } from 'unist-util-visit'; + +// Special characters for marking blanks and separators +const BLANK_MARKER = '\u2420'; +const SEPARATOR = '\u2063'; + +export const preserveCustomSpans = () => (tree) => { + visit(tree, 'element', (node, index, parent) => { + if (isBlankSpan(node)) { + const reservedString = toString(node); + parent.properties['data-blanks'] = parent.properties['data-blanks'] + ? `${parent.properties['data-blanks']}${SEPARATOR}${reservedString}` + : reservedString; + + parent.children.splice(index, 1, { type: 'text', value: BLANK_MARKER }); + } + }); +}; + +export const restoreCustomSpans = () => (tree) => { + visit(tree, 'element', (node) => { + if (node.tagName === 'codesplit') { + let containsBlank = false; + + visit(node, 'element', (nodeInCodesplit) => { + if ( + nodeInCodesplit.tagName === 'code' && + nodeInCodesplit.properties['data-blanks'] + ) { + const blanks = + nodeInCodesplit.properties['data-blanks'].split(SEPARATOR); + restoreBlankSpans(nodeInCodesplit, blanks); + delete nodeInCodesplit.properties['data-blanks']; + containsBlank = true; + } + }); + + if (containsBlank) { + node.properties['data-contains-blank'] = true; + } + } + }); +}; + +const isBlankSpan = (node) => + node.tagName === 'span' && + Array.isArray(node.properties.className) && + node.properties.className.includes('blank'); + +const restoreBlankSpans = (node, blanks) => { + let blankIndex = 0; + + visit(node, 'text', (textNode, index, parent) => { + if (textNode.value.includes(BLANK_MARKER)) { + const parts = textNode.value.split(BLANK_MARKER); + const newNodes = parts.flatMap((part, i) => { + const nodes = part ? [{ type: 'text', value: part }] : []; + if (i < parts.length - 1 && blankIndex < blanks.length) { + nodes.push({ + type: 'element', + tagName: 'span', + properties: { className: ['blank'] }, + children: [{ type: 'text', value: blanks[blankIndex++] }], + }); + } + return nodes; + }); + parent.children.splice(index, 1, ...newNodes); + return index + newNodes.length; + } + }); +}; diff --git a/gatsby/lib/codesplit.mjs b/gatsby/lib/codesplit.mjs index d667ca0..1b71683 100644 --- a/gatsby/lib/codesplit.mjs +++ b/gatsby/lib/codesplit.mjs @@ -1,6 +1,7 @@ import { h } from 'hastscript'; import { visit } from 'unist-util-visit'; import { toHtml } from 'hast-util-to-html'; +import { toString } from 'hast-util-to-string'; import { fromHtml } from 'hast-util-from-html'; /** @@ -22,6 +23,7 @@ export const rehypeCodesplit = () => (tree) => { node.properties.className && node.properties.className.includes('codesplit') ) { + const raw = toString(node); const lines = toHtml(node.children).split('\n'); const lang = node.properties.dataCodeLanguage; @@ -114,11 +116,16 @@ export const rehypeCodesplit = () => (tree) => { return h('div', { className }, [ h('pre', [h('code', { class: ['code', `language-${lang}`] }, code)]), - h('div', { class: ['comment'] }, h('p', fromHtml(comments.join('\n'), { fragment: true }))), + h( + 'div', + { class: ['comment'] }, + h('p', fromHtml(comments.join('\n'), { fragment: true })), + ), ]); }); - node.tagName = 'div'; + node.tagName = 'codesplit'; + node.properties['data-raw'] = raw; node.properties.className = ['codesplit', 'callout', 'not-prose']; node.children = children; } diff --git a/gatsby/lib/parse-content.mjs b/gatsby/lib/parse-content.mjs index 94e52a7..278e038 100644 --- a/gatsby/lib/parse-content.mjs +++ b/gatsby/lib/parse-content.mjs @@ -10,6 +10,7 @@ import rehypeHighlight from 'rehype-highlight'; import { toString } from 'hast-util-to-string'; import { rehypeCodesplit } from './codesplit.mjs'; +import { preserveCustomSpans, restoreCustomSpans } from './blank-span.mjs'; function isHeading(node) { return node.type === 'element' && /^h[1-6]$/i.test(node.tagName); @@ -178,7 +179,9 @@ export function parseContent(html) { .use(replaceMedia) .use(externalLinkInNewTab) .use(rehypeCodesplit) + .use(preserveCustomSpans) .use(rehypeHighlight) + .use(restoreCustomSpans) .use(rehypeSlug) .use(rehypeKatex) .runSync(ast); diff --git a/src/components/Codesplit.js b/src/components/Codesplit.js new file mode 100644 index 0000000..720d30f --- /dev/null +++ b/src/components/Codesplit.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { HiOutlineEye, HiOutlineEyeOff } from 'react-icons/hi'; +import { RiFileCopyLine } from 'react-icons/ri'; + +const LANGUAGE_NAME_MAP = { + javascript: 'JavaScript', + html: 'HTML', +}; + +const Codesplit = (props) => { + const [isAnswerVisible, setIsAnswerVisible] = React.useState(false); + const [isCopied, setIsCopied] = React.useState(false); + + const toggleAnswerHiddenStatus = () => { + setIsAnswerVisible((lastState) => !lastState); + }; + + const copyRaw = () => { + if (isCopied) return; + if (!navigator.clipboard || !navigator.clipboard.writeText) return; + + navigator.clipboard.writeText(props['data-raw']); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1000); + }; + + const displayedLanguageName = + LANGUAGE_NAME_MAP[props['data-code-language']] || + props['data-code-language']; + + const containsBlank = !!props['data-contains-blank']; + + return ( +
+
+
+ {displayedLanguageName} +
+ +
+ {containsBlank && ( + + )} + + +
+
+ + {props.children} +
+ ); +}; + +export default Codesplit; diff --git a/src/layouts/ChapterLayout.js b/src/layouts/ChapterLayout.js index f3265d2..313216b 100644 --- a/src/layouts/ChapterLayout.js +++ b/src/layouts/ChapterLayout.js @@ -9,6 +9,7 @@ import PrevNextButtons from '../components/PrevNextButtons'; import Image from '../components/Image'; import Example from '../components/Example'; import VideoLink from '../components/VideoLink'; +import Codesplit from '../components/Codesplit'; const renderAst = ({ ast, images }) => { visit(ast, { tagName: 'img' }, (node) => { @@ -31,6 +32,7 @@ const renderAst = ({ ast, images }) => { 'gatsby-image': Image, 'embed-example': Example, 'video-link': VideoLink, + 'codesplit': Codesplit, }, }); diff --git a/src/styles/codesplit.css b/src/styles/codesplit.css index 266b39a..3ea99f9 100644 --- a/src/styles/codesplit.css +++ b/src/styles/codesplit.css @@ -9,7 +9,7 @@ } .snip-above > .codesplit::before { - @apply content-icon-scissors absolute -left-3.5 -top-3 h-5 w-5; + @apply absolute -left-3.5 -top-3 h-5 w-5 content-icon-scissors; } .snip-below > .codesplit { @@ -17,7 +17,7 @@ } .snip-below > .codesplit::after { - @apply content-icon-scissors absolute -bottom-2.5 -left-3.5 h-5 w-5; + @apply absolute -bottom-2.5 -left-3.5 h-5 w-5 content-icon-scissors; } .pair { @@ -25,13 +25,21 @@ } .pair.split { - @apply border-noc-200 my-1 flex flex-col-reverse flex-wrap justify-between gap-x-8 gap-y-2 border-l-2 bg-gray-200 py-2 lg:flex-row lg:flex-wrap-reverse; + @apply my-1 flex flex-col-reverse flex-wrap justify-between gap-x-8 gap-y-2 border-l-2 border-noc-200 bg-gray-200 py-2 lg:flex-row lg:flex-wrap-reverse; } .pair code.hljs { @apply bg-transparent p-0 text-sm; } +.pair span.blank { + @apply border-b border-gray-400 text-sm text-transparent; +} + +.is-answer-visible .pair span.blank { + @apply text-[#24292e]; +} + .comment { @apply flex flex-grow text-sm text-gray-600 lg:justify-end; } From 52a44c08eed4c8fc5561d4d72cb791fbae4fc2c8 Mon Sep 17 00:00:00 2001 From: Yifei Gao Date: Tue, 24 Sep 2024 22:24:51 +0800 Subject: [PATCH 3/4] feat: only add the language badge when it's not javascript --- src/components/Codesplit.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/Codesplit.js b/src/components/Codesplit.js index 720d30f..a654643 100644 --- a/src/components/Codesplit.js +++ b/src/components/Codesplit.js @@ -2,9 +2,21 @@ import React from 'react'; import { HiOutlineEye, HiOutlineEyeOff } from 'react-icons/hi'; import { RiFileCopyLine } from 'react-icons/ri'; -const LANGUAGE_NAME_MAP = { - javascript: 'JavaScript', - html: 'HTML', +const LanguageNameBadge = ({ language }) => { + if (!language) return null; + + // Not displaying the language name when it's js + if (language === 'javascript' || language === 'js') return null; + + const LANGUAGE_NAME_ALIAS = { + html: 'HTML', + }; + + return ( +
+ {LANGUAGE_NAME_ALIAS[language] || language} +
+ ); }; const Codesplit = (props) => { @@ -26,10 +38,6 @@ const Codesplit = (props) => { }, 1000); }; - const displayedLanguageName = - LANGUAGE_NAME_MAP[props['data-code-language']] || - props['data-code-language']; - const containsBlank = !!props['data-contains-blank']; return ( @@ -37,11 +45,9 @@ const Codesplit = (props) => { className={`pt-0 ${props.className} ${isAnswerVisible ? 'is-answer-visible' : ''}`} >
-
- {displayedLanguageName} -
+ -
+
{containsBlank && (