From 145e1d2d04a690c97c26ab58f7aa59f2795bbc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Sat, 14 Feb 2026 14:37:14 -0600 Subject: [PATCH] refactor(baoyu-post-to-x): replace hand-rolled markdown parser with marked ecosystem Switch md-to-html from manual line-by-line parsing to marked + front-matter + highlight.js + remark-cjk-friendly for robust markdown conversion with syntax highlighting, proper CJK handling, and standard frontmatter parsing. Co-Authored-By: Claude Opus 4.6 --- skills/baoyu-post-to-x/scripts/bun.lock | 140 +++++++ skills/baoyu-post-to-x/scripts/md-to-html.ts | 362 ++++++++++--------- skills/baoyu-post-to-x/scripts/package.json | 14 + 3 files changed, 343 insertions(+), 173 deletions(-) create mode 100644 skills/baoyu-post-to-x/scripts/bun.lock create mode 100644 skills/baoyu-post-to-x/scripts/package.json diff --git a/skills/baoyu-post-to-x/scripts/bun.lock b/skills/baoyu-post-to-x/scripts/bun.lock new file mode 100644 index 0000000..a8637f0 --- /dev/null +++ b/skills/baoyu-post-to-x/scripts/bun.lock @@ -0,0 +1,140 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "baoyu-post-to-x-scripts", + "dependencies": { + "front-matter": "^4.0.2", + "highlight.js": "^11.11.1", + "marked": "^15.0.6", + "remark-cjk-friendly": "^1.1.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + }, + }, + }, + "packages": { + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="], + + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + } +} diff --git a/skills/baoyu-post-to-x/scripts/md-to-html.ts b/skills/baoyu-post-to-x/scripts/md-to-html.ts index 9e6e8da..d3a832c 100644 --- a/skills/baoyu-post-to-x/scripts/md-to-html.ts +++ b/skills/baoyu-post-to-x/scripts/md-to-html.ts @@ -7,6 +7,14 @@ import path from 'node:path'; import process from 'node:process'; import { createHash } from 'node:crypto'; +import frontMatter from 'front-matter'; +import hljs from 'highlight.js/lib/common'; +import { Lexer, Marked, type RendererObject, type Tokens } from 'marked'; +import { unified } from 'unified'; +import remarkCjkFriendly from 'remark-cjk-friendly'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; + interface ImageInfo { placeholder: string; localPath: string; @@ -22,25 +30,80 @@ interface ParsedMarkdown { totalBlocks: number; } -function parseFrontmatter(content: string): { frontmatter: Record; body: string } { - const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); - if (!match) return { frontmatter: {}, body: content }; +type FrontmatterFields = Record; - const frontmatter: Record = {}; - const lines = match[1]!.split('\n'); - for (const line of lines) { - const colonIdx = line.indexOf(':'); - if (colonIdx > 0) { - const key = line.slice(0, colonIdx).trim(); - let value = line.slice(colonIdx + 1).trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); +function parseFrontmatter(content: string): { frontmatter: FrontmatterFields; body: string } { + try { + const parsed = frontMatter(content); + return { + frontmatter: parsed.attributes ?? {}, + body: parsed.body, + }; + } catch { + return { frontmatter: {}, body: content }; + } +} + +function stripWrappingQuotes(value: string): string { + if (!value) return value; + const doubleQuoted = value.startsWith('"') && value.endsWith('"'); + const singleQuoted = value.startsWith("'") && value.endsWith("'"); + const cjkDoubleQuoted = value.startsWith('\u201c') && value.endsWith('\u201d'); + const cjkSingleQuoted = value.startsWith('\u2018') && value.endsWith('\u2019'); + if (doubleQuoted || singleQuoted || cjkDoubleQuoted || cjkSingleQuoted) { + return value.slice(1, -1).trim(); + } + return value.trim(); +} + +function toFrontmatterString(value: unknown): string | undefined { + if (typeof value === 'string') { + return stripWrappingQuotes(value); + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + return undefined; +} + +function pickFirstString(frontmatter: FrontmatterFields, keys: string[]): string | undefined { + for (const key of keys) { + const value = toFrontmatterString(frontmatter[key]); + if (value) return value; + } + return undefined; +} + +function findCoverImageNearMarkdown(baseDir: string): string | null { + const candidateDirs = [baseDir, path.join(baseDir, 'imgs')]; + const coverPattern = /^cover\.(png|jpe?g|webp)$/i; + + for (const dir of candidateDirs) { + try { + if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { + continue; } - frontmatter[key] = value; + + const match = fs.readdirSync(dir).find((entry) => coverPattern.test(entry)); + if (match) { + return path.join(dir, match); + } + } catch { + continue; } } - return { frontmatter, body: match[2]! }; + return null; +} + +function extractTitleFromMarkdown(markdown: string): string { + const tokens = Lexer.lex(markdown, { gfm: true, breaks: true }); + for (const token of tokens) { + if (token.type === 'heading' && token.depth === 1) { + return stripWrappingQuotes(token.text); + } + } + return ''; } function downloadFile(url: string, destPath: string): Promise { @@ -116,141 +179,99 @@ function escapeHtml(text: string): string { .replace(/&/g, '&') .replace(//g, '>') - .replace(/"/g, '"'); + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function highlightCode(code: string, lang: string): string { + try { + if (lang && hljs.getLanguage(lang)) { + return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value; + } + return hljs.highlightAuto(code).value; + } catch { + return escapeHtml(code); + } +} + +function preprocessCjkMarkdown(markdown: string): string { + try { + const processor = unified() + .use(remarkParse) + .use(remarkCjkFriendly) + .use(remarkStringify); + + const result = String(processor.processSync(markdown)); + return result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16))); + } catch { + return markdown; + } } function convertMarkdownToHtml(markdown: string, imageCallback: (src: string, alt: string) => string): { html: string; totalBlocks: number } { - const lines = markdown.split('\n'); - const blocks: string[] = []; - let inCodeBlock = false; - let codeBlockContent: string[] = []; - let inList = false; - let listItems: string[] = []; - let listType: 'ul' | 'ol' = 'ul'; + const preprocessedMarkdown = preprocessCjkMarkdown(markdown); + const blockTokens = Lexer.lex(preprocessedMarkdown, { gfm: true, breaks: true }); - const flushList = () => { - if (listItems.length > 0) { - const tag = listType === 'ol' ? 'ol' : 'ul'; - blocks.push(`<${tag}>${listItems.map((item) => `
  • ${item}
  • `).join('')}`); - listItems = []; - inList = false; - } + const renderer: RendererObject = { + heading({ depth, tokens }: Tokens.Heading): string { + if (depth === 1) { + return ''; + } + return `

    ${this.parser.parseInline(tokens)}

    `; + }, + + paragraph({ tokens }: Tokens.Paragraph): string { + const text = this.parser.parseInline(tokens).trim(); + if (!text) return ''; + return `

    ${text}

    `; + }, + + blockquote({ tokens }: Tokens.Blockquote): string { + return `
    ${this.parser.parse(tokens)}
    `; + }, + + code({ text, lang = '' }: Tokens.Code): string { + const language = lang.split(/\s+/)[0]!.toLowerCase(); + const source = text.replace(/\n$/, ''); + const highlighted = highlightCode(source, language).replace(/\n/g, '
    '); + const label = language ? `[${escapeHtml(language)}]
    ` : ''; + return `
    ${label}${highlighted}
    `; + }, + + image({ href, text }: Tokens.Image): string { + if (!href) return ''; + return imageCallback(href, text ?? ''); + }, + + link({ href, title, tokens, text }: Tokens.Link): string { + const label = tokens?.length ? this.parser.parseInline(tokens) : escapeHtml(text || href || ''); + if (!href) return label; + + const titleAttr = title ? ` title="${escapeHtml(title)}"` : ''; + return `${label}`; + }, }; - const processInline = (text: string): string => { - // Bold - text = text.replace(/\*\*(.+?)\*\*/g, '$1'); - text = text.replace(/__(.+?)__/g, '$1'); + const parser = new Marked({ + gfm: true, + breaks: true, + }); + parser.use({ renderer }); - // Italic - text = text.replace(/\*(.+?)\*/g, '$1'); - text = text.replace(/_(.+?)_/g, '$1'); - - // Links - text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - - // Inline code - text = text.replace(/`([^`]+)`/g, '$1'); - - return text; - }; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]!; - - // Code block - if (line.startsWith('```')) { - if (inCodeBlock) { - // X doesn't support
    , convert to blockquote
    -        const codeContent = codeBlockContent.map((l) => escapeHtml(l)).join('
    '); - blocks.push(`
    ${codeContent}
    `); - codeBlockContent = []; - inCodeBlock = false; - } else { - flushList(); - inCodeBlock = true; - } - continue; - } - - if (inCodeBlock) { - codeBlockContent.push(line); - continue; - } - - // Empty line - if (line.trim() === '') { - flushList(); - continue; - } - - // Image - const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/); - if (imgMatch) { - flushList(); - const placeholder = imageCallback(imgMatch[2]!, imgMatch[1]!); - blocks.push(`

    ${placeholder}

    `); - continue; - } - - // Heading (H1 is title, skip it; H2-H6 become H2) - const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); - if (headingMatch) { - flushList(); - const level = headingMatch[1]!.length; - if (level === 1) continue; // Skip H1, it's the title - blocks.push(`

    ${processInline(headingMatch[2]!)}

    `); - continue; - } - - // Blockquote - if (line.startsWith('> ')) { - flushList(); - blocks.push(`
    ${processInline(line.slice(2))}
    `); - continue; - } - - // Unordered list - const ulMatch = line.match(/^[-*]\s+(.+)$/); - if (ulMatch) { - if (!inList || listType !== 'ul') { - flushList(); - inList = true; - listType = 'ul'; - } - listItems.push(processInline(ulMatch[1]!)); - continue; - } - - // Ordered list - const olMatch = line.match(/^\d+\.\s+(.+)$/); - if (olMatch) { - if (!inList || listType !== 'ol') { - flushList(); - inList = true; - listType = 'ol'; - } - listItems.push(processInline(olMatch[1]!)); - continue; - } - - // Horizontal rule - if (/^[-*_]{3,}\s*$/.test(line)) { - flushList(); - blocks.push('
    '); - continue; - } - - // Regular paragraph - flushList(); - blocks.push(`

    ${processInline(line)}

    `); + const rendered = parser.parse(preprocessedMarkdown); + if (typeof rendered !== 'string') { + throw new Error('Unexpected async markdown parse result'); } - flushList(); + const totalBlocks = blockTokens.filter((token) => { + if (token.type === 'space') return false; + if (token.type === 'heading' && token.depth === 1) return false; + return true; + }).length; return { - html: blocks.join('\n'), - totalBlocks: blocks.length, + html: rendered, + totalBlocks, }; } @@ -266,59 +287,58 @@ export async function parseMarkdown( const { frontmatter, body } = parseFrontmatter(content); - // Extract title from frontmatter, option, or first H1 - let title = options?.title ?? frontmatter.title ?? ''; + let title = stripWrappingQuotes(options?.title ?? '') || pickFirstString(frontmatter, ['title']) || ''; if (!title) { - const h1Match = body.match(/^#\s+(.+)$/m); - if (h1Match) title = h1Match[1]!; + title = extractTitleFromMarkdown(body); + } + if (!title) { + title = path.basename(markdownPath, path.extname(markdownPath)); } - // Extract cover image from frontmatter or option - let coverImagePath = options?.coverImage ?? frontmatter.cover_image ?? frontmatter.coverImage ?? frontmatter.cover ?? frontmatter.image ?? frontmatter.featureImage ?? frontmatter.feature_image ?? null; + let coverImagePath = stripWrappingQuotes(options?.coverImage ?? '') || pickFirstString(frontmatter, [ + 'cover_image', + 'coverImage', + 'cover', + 'image', + 'featureImage', + 'feature_image', + ]) || null; + if (!coverImagePath) { + coverImagePath = findCoverImageNearMarkdown(baseDir); + } const images: Array<{ src: string; alt: string; blockIndex: number }> = []; let imageCounter = 0; const { html, totalBlocks } = convertMarkdownToHtml(body, (src, alt) => { const placeholder = `XIMGPH_${++imageCounter}`; - const currentBlockIndex = images.length; // Will be set properly after HTML generation - - images.push({ src, alt, blockIndex: -1 }); // blockIndex set later + images.push({ src, alt, blockIndex: -1 }); return placeholder; }); - // Update block indices by finding placeholders in HTML const htmlLines = html.split('\n'); - let blockIdx = 0; - for (const line of htmlLines) { - for (let i = 0; i < images.length; i++) { - const placeholder = `XIMGPH_${i + 1}`; - if (line.includes(placeholder)) { - images[i]!.blockIndex = blockIdx; + for (let i = 0; i < images.length; i++) { + const placeholder = `XIMGPH_${i + 1}`; + for (let lineIndex = 0; lineIndex < htmlLines.length; lineIndex++) { + const regex = new RegExp(`\\b${placeholder}\\b`); + if (regex.test(htmlLines[lineIndex]!)) { + images[i]!.blockIndex = lineIndex; + break; } } - blockIdx++; } - // Resolve image paths (download remote, resolve relative) const contentImages: ImageInfo[] = []; - let isFirstImage = true; - let coverPlaceholder: string | null = null; + let firstImageAsCover: string | null = null; for (let i = 0; i < images.length; i++) { const img = images[i]!; const localPath = await resolveImagePath(img.src, baseDir, tempDir); - // First image becomes cover if no cover specified - if (isFirstImage && !coverImagePath) { - coverImagePath = localPath; - coverPlaceholder = `XIMGPH_${i + 1}`; - isFirstImage = false; - // Don't add to contentImages, it's the cover - continue; + if (i === 0 && !coverImagePath) { + firstImageAsCover = localPath; } - isFirstImage = false; contentImages.push({ placeholder: `XIMGPH_${i + 1}`, localPath, @@ -327,17 +347,13 @@ export async function parseMarkdown( }); } - // Remove cover placeholder from HTML if first image was used as cover - let finalHtml = html; - if (coverPlaceholder) { - // Remove the placeholder and its containing

    tag - finalHtml = finalHtml.replace(new RegExp(`

    ${coverPlaceholder}

    \\n?`, 'g'), ''); - } + const finalHtml = html.replace(/\n{3,}/g, '\n\n').trim(); - // Resolve cover image path let resolvedCoverImage: string | null = null; if (coverImagePath) { resolvedCoverImage = await resolveImagePath(coverImagePath, baseDir, tempDir); + } else if (firstImageAsCover) { + resolvedCoverImage = firstImageAsCover; } return { diff --git a/skills/baoyu-post-to-x/scripts/package.json b/skills/baoyu-post-to-x/scripts/package.json new file mode 100644 index 0000000..32d0772 --- /dev/null +++ b/skills/baoyu-post-to-x/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "baoyu-post-to-x-scripts", + "private": true, + "type": "module", + "dependencies": { + "front-matter": "^4.0.2", + "highlight.js": "^11.11.1", + "marked": "^15.0.6", + "remark-cjk-friendly": "^1.1.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5" + } +}