oceanpress 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/cli.js CHANGED
@@ -77,8 +77,22 @@ import { computed, reactive, watch } from "vue";
77
77
  // package.json
78
78
  var package_default = {
79
79
  name: "oceanpress",
80
- version: "1.0.10",
80
+ version: "1.0.13",
81
81
  type: "module",
82
+ description: "\u4ECE\u601D\u6E90\u7B14\u8BB0\u672C\u751F\u6210\u9759\u6001\u7AD9\u70B9\u7684\u5DE5\u5177",
83
+ author: "siyuan-note",
84
+ license: "MIT",
85
+ keywords: [
86
+ "siyuan",
87
+ "static-site-generator",
88
+ "markdown",
89
+ "blog",
90
+ "ssg"
91
+ ],
92
+ homepage: "https://github.com/siyuan-note/oceanpress#readme",
93
+ bugs: {
94
+ url: "https://github.com/siyuan-note/oceanpress/issues"
95
+ },
82
96
  scripts: {
83
97
  dev: "vite",
84
98
  cli: "tsx ./src/cli.ts",
@@ -105,6 +119,10 @@ var package_default = {
105
119
  "dist-cli",
106
120
  "*.md"
107
121
  ],
122
+ repository: {
123
+ type: "git",
124
+ url: "https://github.com/siyuan-note/oceanpress.git"
125
+ },
108
126
  dependencies: {
109
127
  "@aws-sdk/client-s3": "^3.873.0",
110
128
  "@hono/node-server": "^1.19.0",
@@ -119,12 +137,14 @@ var package_default = {
119
137
  octokit: "^5.0.3",
120
138
  superjson: "^2.2.2",
121
139
  tsx: "^4.20.4",
140
+ turndown: "^7.2.2",
122
141
  vditor: "^3.11.1",
123
142
  vue: "^3.5.19",
124
143
  "vue-router": "^4.5.1",
125
144
  "zstd-codec": "^0.1.5"
126
145
  },
127
146
  devDependencies: {
147
+ "@types/turndown": "^5.0.6",
128
148
  "@vitejs/plugin-vue": "^6.0.1",
129
149
  "@vitejs/plugin-vue-jsx": "^5.0.1",
130
150
  "dependency-cruiser": "^17.0.1",
@@ -248,6 +268,17 @@ var defaultConfig = {
248
268
  apiKey: "",
249
269
  indexName: ""
250
270
  },
271
+ /** Markdown 镜像导出配置 */
272
+ markdownMirror: {
273
+ enable: false,
274
+ outputDir: "",
275
+ includeAssets: false,
276
+ watchMode: false,
277
+ /** 定时同步间隔(毫秒) */
278
+ watchInterval: 6e4,
279
+ /** 是否移除头部和底部(侧边栏、导航、footer 等) */
280
+ removeTemplate: false
281
+ },
251
282
  /** html模板嵌入代码块,会将此处配置中的代码嵌入到生成的html所对应的位置 */
252
283
  embedCode: {
253
284
  head: "",
@@ -518,6 +549,237 @@ var s3_uploads = async function* (tree, config) {
518
549
  }
519
550
  };
520
551
 
552
+ // src/plugins/markdown_mirror/plugin.ts
553
+ import TurndownService from "turndown";
554
+ var MarkdownMirrorPlugin = class {
555
+ constructor(config) {
556
+ this.config = config;
557
+ __publicField(this, "name", "MarkdownMirrorPlugin");
558
+ /** 拦截 build,添加 Markdown 导出回调 */
559
+ __publicField(this, "build", function([config, otherConfig], next) {
560
+ return next(config, {
561
+ ...otherConfig,
562
+ beforeFileTree: async (tree, effectApi) => {
563
+ if (otherConfig?.beforeFileTree) {
564
+ await otherConfig.beforeFileTree(tree, effectApi);
565
+ }
566
+ if (config.markdownMirror?.enable) {
567
+ effectApi.log("\n=== \u5F00\u59CB Markdown \u955C\u50CF\u5BFC\u51FA ===");
568
+ try {
569
+ convertHtmlToMarkdown(
570
+ tree,
571
+ config.markdownMirror.includeAssets
572
+ );
573
+ effectApi.log("=== Markdown \u955C\u50CF\u5BFC\u51FA\u5B8C\u6210 ===\n");
574
+ } catch (error) {
575
+ effectApi.log("\u274C Markdown \u955C\u50CF\u5BFC\u51FA\u5931\u8D25: " + error);
576
+ }
577
+ }
578
+ },
579
+ onFileTree: async (tree, effectApi) => {
580
+ if (otherConfig?.onFileTree) {
581
+ await otherConfig.onFileTree(tree, effectApi);
582
+ }
583
+ }
584
+ });
585
+ });
586
+ }
587
+ };
588
+ function buildIdToHeadingMap(htmlContent) {
589
+ const idToHeading = /* @__PURE__ */ new Map();
590
+ const headingRegex = /<h([1-6])\s+id="([^"]+)"[^>]*>(.*?)<\/h\1>/gi;
591
+ let match;
592
+ while ((match = headingRegex.exec(htmlContent)) !== null) {
593
+ const id = match[2];
594
+ const headingText = match[3].replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
595
+ idToHeading.set(id, headingText);
596
+ }
597
+ return idToHeading;
598
+ }
599
+ function headingToAnchor(headingText) {
600
+ return headingText.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
601
+ }
602
+ function normalizePath(relativePath) {
603
+ let normalized = relativePath.replace(/^\.\//, "");
604
+ while (normalized.startsWith("../")) {
605
+ normalized = normalized.slice(3);
606
+ }
607
+ return normalized;
608
+ }
609
+ function convertHtmlToMarkdown(tree, includeAssets = false) {
610
+ const allFiles = Object.keys(tree);
611
+ console.log(`\u{1F4C2} \u6587\u4EF6\u6811\u4E2D\u5171\u6709 ${allFiles.length} \u4E2A\u6587\u4EF6`);
612
+ if (allFiles.length > 0) {
613
+ console.log("\u{1F4C2} \u524D 10 \u4E2A\u6587\u4EF6:", allFiles.slice(0, 10));
614
+ }
615
+ const globalIdToHeading = /* @__PURE__ */ new Map();
616
+ const htmlFiles = Object.entries(tree).filter(([filePath]) => filePath.endsWith(".html"));
617
+ console.log("\u{1F4D6} \u6B63\u5728\u626B\u63CF HTML \u6587\u4EF6\u4EE5\u5EFA\u7ACB\u6807\u9898\u6620\u5C04...");
618
+ for (const [htmlPath, htmlContent] of htmlFiles) {
619
+ const htmlStr = htmlContent.toString();
620
+ const idToHeading = buildIdToHeadingMap(htmlStr);
621
+ for (const [id, heading] of idToHeading.entries()) {
622
+ const mdPath = htmlPath.replace(/\.html$/, ".md");
623
+ globalIdToHeading.set(`${mdPath}:${id}`, heading);
624
+ globalIdToHeading.set(`${htmlPath}:${id}`, heading);
625
+ globalIdToHeading.set(`./${mdPath}:${id}`, heading);
626
+ globalIdToHeading.set(`./${htmlPath}:${id}`, heading);
627
+ }
628
+ }
629
+ console.log(`\u2705 \u5EFA\u7ACB\u4E86 ${globalIdToHeading.size} \u4E2A\u6807\u9898\u6620\u5C04
630
+ `);
631
+ const turndownService = new TurndownService({
632
+ headingStyle: "atx",
633
+ codeBlockStyle: "fenced",
634
+ bulletListMarker: "-"
635
+ });
636
+ turndownService.addRule("remove-sidebars", {
637
+ filter: (node) => {
638
+ return node.id === "oceanpress-left-sidebar" || node.id === "oceanpress-right-sidebar";
639
+ },
640
+ replacement: () => ""
641
+ });
642
+ turndownService.addRule("remove-html-metadata", {
643
+ filter: (node) => {
644
+ return ["head", "style", "script", "noscript", "link", "meta", "title"].includes(
645
+ node.nodeName?.toLowerCase()
646
+ );
647
+ },
648
+ replacement: () => ""
649
+ });
650
+ turndownService.addRule("remove-json-ld", {
651
+ filter: (node) => {
652
+ return node.nodeName === "SCRIPT" && node.getAttribute("type") === "application/ld+json";
653
+ },
654
+ replacement: () => ""
655
+ });
656
+ turndownService.addRule("remove-siyuan-config", {
657
+ filter: (node) => {
658
+ return node.nodeName === "SCRIPT" && node.textContent?.includes("window.siyuan");
659
+ },
660
+ replacement: () => ""
661
+ });
662
+ turndownService.addRule("remove-footer", {
663
+ filter: (node) => {
664
+ return node.nodeName === "FOOTER" || node.nodeName === "DIV" && node.classList?.contains("theme-aware-footer");
665
+ },
666
+ replacement: () => ""
667
+ });
668
+ turndownService.addRule("remove-home-link", {
669
+ filter: (node) => {
670
+ return node.nodeName === "A" && node.getAttribute("href") === "/";
671
+ },
672
+ replacement: () => ""
673
+ });
674
+ turndownService.addRule("convert-internal-links", {
675
+ filter: (node) => {
676
+ return node.nodeName === "A" && node.getAttribute("href")?.includes(".html");
677
+ },
678
+ replacement: (content, node) => {
679
+ const href = node.getAttribute("href");
680
+ if (!href) return content;
681
+ const linkMatch = href.match(/^(.+?\.html)(#.+)?$/);
682
+ if (!linkMatch) return `[${content}](${href})`;
683
+ const htmlPath = linkMatch[1];
684
+ const anchor = linkMatch[2] || "";
685
+ const normalizedHtmlPath = normalizePath(htmlPath);
686
+ const mdPath = normalizedHtmlPath.replace(/\.html$/, ".md");
687
+ if (anchor) {
688
+ const anchorId = anchor.slice(1);
689
+ let headingText;
690
+ const possiblePaths = [
691
+ `/${mdPath}:${anchorId}`,
692
+ // /工具/blender.md:xxx
693
+ `${mdPath}:${anchorId}`,
694
+ // 工具/blender.md:xxx
695
+ `/${normalizedHtmlPath}:${anchorId}`,
696
+ `${normalizedHtmlPath}:${anchorId}`
697
+ ];
698
+ for (const possiblePath of possiblePaths) {
699
+ if (globalIdToHeading.has(possiblePath)) {
700
+ headingText = globalIdToHeading.get(possiblePath);
701
+ break;
702
+ }
703
+ }
704
+ if (headingText) {
705
+ const headingAnchor = headingToAnchor(headingText);
706
+ const prefix2 = href.startsWith("../") ? "../" : href.startsWith("./") ? "./" : "";
707
+ return `[${content}](${prefix2}${mdPath}#${headingAnchor})`;
708
+ } else {
709
+ if (Math.random() < 0.05) {
710
+ console.warn(`\u26A0\uFE0F \u672A\u627E\u5230\u6807\u9898\u6620\u5C04: ${href}`);
711
+ console.warn(` \u5C1D\u8BD5\u7684\u8DEF\u5F84:`, possiblePaths.slice(0, 2));
712
+ }
713
+ }
714
+ }
715
+ const prefix = href.startsWith("../") ? "../" : href.startsWith("./") ? "./" : "";
716
+ return `[${content}](${prefix}${mdPath})`;
717
+ }
718
+ });
719
+ turndownService.addRule("convert-code-blocks", {
720
+ filter: (node) => {
721
+ return node.nodeName === "DIV" && node.classList?.contains("hljs");
722
+ },
723
+ replacement: (_content, node) => {
724
+ const code = node.textContent || "";
725
+ let language = "";
726
+ const previousSibling = node.previousSibling;
727
+ if (previousSibling && previousSibling.nodeName === "DIV") {
728
+ const actionDiv = previousSibling.querySelector(".protyle-action--first");
729
+ if (actionDiv) {
730
+ const langText = actionDiv.textContent?.trim();
731
+ if (langText && langText !== "Copy") {
732
+ language = langText.toLowerCase();
733
+ }
734
+ }
735
+ }
736
+ if (!language) {
737
+ if (code.includes("function ") || code.includes("const ") || code.includes("let ") || code.includes("=>") || code.includes("interface ")) {
738
+ language = "typescript";
739
+ } else if (code.includes("def ") || code.includes("import ") || code.includes("class ")) {
740
+ language = "python";
741
+ } else if (code.includes("SELECT ") || code.includes("INSERT ") || code.includes("UPDATE ")) {
742
+ language = "sql";
743
+ }
744
+ }
745
+ return `
746
+ \`\`\`${language}
747
+ ${code.trim()}
748
+ \`\`\`
749
+ `;
750
+ }
751
+ });
752
+ console.log(`\u{1F4C4} \u627E\u5230 ${htmlFiles.length} \u4E2A HTML \u6587\u4EF6
753
+ `);
754
+ let convertedCount = 0;
755
+ for (const [htmlPath, htmlContent] of htmlFiles) {
756
+ try {
757
+ const mdPath = htmlPath.replace(/\.html$/, ".md");
758
+ const htmlStr = htmlContent.toString();
759
+ let markdown = turndownService.turndown(htmlStr);
760
+ markdown = markdown.trimStart();
761
+ markdown = markdown.replace(/\n([a-z]+)\n\n(```[a-z]+\n)/g, "\n$2");
762
+ tree[mdPath] = markdown;
763
+ delete tree[htmlPath];
764
+ convertedCount++;
765
+ console.log(`\u2705 [${convertedCount}] ${htmlPath} \u2192 ${mdPath}`);
766
+ } catch (error) {
767
+ console.error(`\u274C \u8F6C\u6362\u5931\u8D25: ${htmlPath}`, error);
768
+ }
769
+ }
770
+ console.log(`
771
+ \u{1F389} \u6210\u529F\u8F6C\u6362 ${convertedCount} \u4E2A HTML \u6587\u4EF6\u4E3A Markdown`);
772
+ if (!includeAssets) {
773
+ const assetPaths = Object.keys(tree).filter((path) => path.startsWith("assets/"));
774
+ assetPaths.forEach((assetPath) => {
775
+ delete tree[assetPath];
776
+ });
777
+ if (assetPaths.length > 0) {
778
+ console.log(`\u{1F5D1}\uFE0F \u5DF2\u4ECE\u6587\u4EF6\u6811\u4E2D\u79FB\u9664 ${assetPaths.length} \u4E2A\u8D44\u6E90\u6587\u4EF6`);
779
+ }
780
+ }
781
+ }
782
+
521
783
  // src/core/build.ts
522
784
  import { Effect as Effect4 } from "effect";
523
785
 
@@ -882,6 +1144,101 @@ function sitemap_xml(docArr, config) {
882
1144
  }
883
1145
 
884
1146
  // src/core/seo.ts
1147
+ var simplifiedStopWords = /* @__PURE__ */ new Set([
1148
+ "\u7684",
1149
+ "\u4E86",
1150
+ "\u5728",
1151
+ "\u662F",
1152
+ "\u6211",
1153
+ "\u6709",
1154
+ "\u548C",
1155
+ "\u5C31",
1156
+ "\u4E0D",
1157
+ "\u4EBA",
1158
+ "\u90FD",
1159
+ "\u4E00",
1160
+ "\u4E2A",
1161
+ "\u4E0A",
1162
+ "\u4E5F",
1163
+ "\u5F88",
1164
+ "\u5230",
1165
+ "\u8BF4",
1166
+ "\u8981",
1167
+ "\u53BB",
1168
+ "\u4F60",
1169
+ "\u4F1A",
1170
+ "\u7740",
1171
+ "\u6CA1\u6709",
1172
+ "\u770B",
1173
+ "\u597D",
1174
+ "\u81EA\u5DF1",
1175
+ "\u8FD9",
1176
+ "\u90A3",
1177
+ "\u73B0\u5728",
1178
+ "\u53EF\u4EE5",
1179
+ "\u4F46\u662F",
1180
+ "\u8FD8\u662F",
1181
+ "\u56E0\u4E3A",
1182
+ "\u4EC0\u4E48",
1183
+ "\u5982\u679C",
1184
+ "\u6240\u4EE5",
1185
+ "the",
1186
+ "a",
1187
+ "an",
1188
+ "and",
1189
+ "or",
1190
+ "but",
1191
+ "in",
1192
+ "on",
1193
+ "at",
1194
+ "to",
1195
+ "for",
1196
+ "of",
1197
+ "with",
1198
+ "by",
1199
+ "is",
1200
+ "are",
1201
+ "was",
1202
+ "were",
1203
+ "be",
1204
+ "been",
1205
+ "being",
1206
+ "have",
1207
+ "has",
1208
+ "had",
1209
+ "do",
1210
+ "does",
1211
+ "did",
1212
+ "will",
1213
+ "would",
1214
+ "could",
1215
+ "should",
1216
+ "may",
1217
+ "might",
1218
+ "must",
1219
+ "can",
1220
+ "this",
1221
+ "that",
1222
+ "these",
1223
+ "those",
1224
+ "i",
1225
+ "you",
1226
+ "he",
1227
+ "she",
1228
+ "it",
1229
+ "we",
1230
+ "they"
1231
+ ]);
1232
+ var htmlCleanupCache = /* @__PURE__ */ new Map();
1233
+ var MAX_CACHE_SIZE = 500;
1234
+ function manageCacheSize() {
1235
+ if (htmlCleanupCache.size > MAX_CACHE_SIZE) {
1236
+ const firstKey = htmlCleanupCache.keys().next().value;
1237
+ if (firstKey) {
1238
+ htmlCleanupCache.delete(firstKey);
1239
+ }
1240
+ }
1241
+ }
885
1242
  function extractDateFromId(id) {
886
1243
  if (!id || id.length < 14) return (/* @__PURE__ */ new Date()).toISOString();
887
1244
  try {
@@ -920,369 +1277,35 @@ function formatDate(dateString) {
920
1277
  return (/* @__PURE__ */ new Date()).toISOString();
921
1278
  }
922
1279
  }
923
- var TFIDFKeywordExtractor = class {
924
- constructor() {
925
- __publicField(this, "stopWords", /* @__PURE__ */ new Set([
926
- // 中文停用词
927
- "\u7684",
928
- "\u4E86",
929
- "\u5728",
930
- "\u662F",
931
- "\u6211",
932
- "\u6709",
933
- "\u548C",
934
- "\u5C31",
935
- "\u4E0D",
936
- "\u4EBA",
937
- "\u90FD",
938
- "\u4E00",
939
- "\u4E2A",
940
- "\u4E0A",
941
- "\u4E5F",
942
- "\u5F88",
943
- "\u5230",
944
- "\u8BF4",
945
- "\u8981",
946
- "\u53BB",
947
- "\u4F60",
948
- "\u4F1A",
949
- "\u7740",
950
- "\u6CA1\u6709",
951
- "\u770B",
952
- "\u597D",
953
- "\u81EA\u5DF1",
954
- "\u8FD9",
955
- "\u90A3",
956
- "\u73B0\u5728",
957
- "\u53EF\u4EE5",
958
- "\u4F46\u662F",
959
- "\u8FD8\u662F",
960
- "\u56E0\u4E3A",
961
- "\u4EC0\u4E48",
962
- "\u5982\u679C",
963
- "\u6240\u4EE5",
964
- "\u5BF9\u4E8E",
965
- "\u5173\u4E8E",
966
- "\u901A\u8FC7",
967
- "\u8FDB\u884C",
968
- "\u57FA\u4E8E",
969
- "\u4EE5\u53CA",
970
- "\u6216\u8005",
971
- "\u800C\u4E14",
972
- "\u7136\u540E",
973
- "\u53EA\u662F",
974
- "\u5DF2\u7ECF",
975
- "\u6B63\u5728",
976
- "\u5E94\u8BE5",
977
- "\u80FD\u591F",
978
- "\u9700\u8981",
979
- "\u53EF\u80FD",
980
- "\u4E00\u5B9A",
981
- "\u8FD9\u6837",
982
- "\u90A3\u6837",
983
- "\u600E\u4E48",
984
- "\u4E3A\u4EC0\u4E48",
985
- "\u54EA\u91CC",
986
- "\u54EA\u4E2A",
987
- "\u591A\u5C11",
988
- "\u51E0\u4E2A",
989
- "\u4EC0\u4E48",
990
- "\u600E\u4E48",
991
- "\u5982\u4F55",
992
- "\u4E3A\u4EC0\u4E48",
993
- // 英文停用词
994
- "the",
995
- "a",
996
- "an",
997
- "and",
998
- "or",
999
- "but",
1000
- "in",
1001
- "on",
1002
- "at",
1003
- "to",
1004
- "for",
1005
- "of",
1006
- "with",
1007
- "by",
1008
- "is",
1009
- "are",
1010
- "was",
1011
- "were",
1012
- "be",
1013
- "been",
1014
- "being",
1015
- "have",
1016
- "has",
1017
- "had",
1018
- "do",
1019
- "does",
1020
- "did",
1021
- "will",
1022
- "would",
1023
- "could",
1024
- "should",
1025
- "may",
1026
- "might",
1027
- "must",
1028
- "can",
1029
- "this",
1030
- "that",
1031
- "these",
1032
- "those",
1033
- "i",
1034
- "you",
1035
- "he",
1036
- "she",
1037
- "it",
1038
- "we",
1039
- "they",
1040
- "me",
1041
- "him",
1042
- "her",
1043
- "us",
1044
- "them",
1045
- "my",
1046
- "your",
1047
- "his",
1048
- "its",
1049
- "our",
1050
- "their",
1051
- "not",
1052
- "no",
1053
- "yes",
1054
- "so",
1055
- "if",
1056
- "when",
1057
- "where",
1058
- "how",
1059
- "why",
1060
- "what",
1061
- "which",
1062
- "who",
1063
- "whom",
1064
- "there",
1065
- "here"
1066
- ]));
1067
- }
1068
- /**
1069
- * 计算词频 (TF)
1070
- */
1071
- calculateTermFrequency(text) {
1072
- const words = this.tokenize(text);
1073
- const tf = /* @__PURE__ */ new Map();
1074
- const totalWords = words.length;
1075
- for (const word of words) {
1076
- tf.set(word, (tf.get(word) || 0) + 1);
1077
- }
1078
- for (const [word, count] of tf) {
1079
- tf.set(word, count / totalWords);
1080
- }
1081
- return tf;
1082
- }
1083
- /**
1084
- * 分词处理(针对中文和英文优化)
1085
- */
1086
- tokenize(text) {
1087
- const tokens = [];
1088
- const plainText = text.replace(/<[^>]*>/g, " ").replace(/&[^;]+;/g, " ").replace(/\s+/g, " ").trim();
1089
- const englishWords = plainText.match(/[a-zA-Z]{3,}/g) || [];
1090
- tokens.push(...englishWords.map((word) => word.toLowerCase()).filter((word) => !this.stopWords.has(word)));
1091
- const camelCaseWords = plainText.match(/[a-z]+[A-Z][a-zA-Z0-9]*|[A-Z][a-z0-9]+[A-Z][a-zA-Z0-9]*/g) || [];
1092
- tokens.push(...camelCaseWords.map((word) => word.toLowerCase()));
1093
- const chineseTokens = this.extractChineseTokens(plainText);
1094
- tokens.push(...chineseTokens);
1095
- const filteredTokens = tokens.filter((token) => {
1096
- if (token.length < 2) return false;
1097
- const htmlRelatedWords = ["div", "span", "class", "id", "type", "data", "href", "src", "alt", "title", "style", "width", "height", "rootid", "endid", "nodedocument", "ezcqbj", "cktc", "quot"];
1098
- if (htmlRelatedWords.includes(token.toLowerCase())) {
1099
- return false;
1100
- }
1101
- if (token.match(/^[a-f0-9]{6,}$/)) return false;
1102
- if (/[\u4e00-\u9fa5]/.test(token) && token.length < 2) {
1103
- return false;
1104
- }
1105
- return true;
1106
- });
1107
- return filteredTokens;
1108
- }
1109
- /**
1110
- * 智能中文分词 - 基于文本特征分析
1111
- */
1112
- extractChineseTokens(text) {
1113
- const tokens = [];
1114
- const chineseText = text.replace(/[^\u4e00-\u9fa5\s]/g, " ");
1115
- const wordPatterns = [
1116
- /[\u4e00-\u9fa5]{4}/g,
1117
- // 4字词汇
1118
- /[\u4e00-\u9fa5]{3}/g,
1119
- // 3字词汇
1120
- /[\u4e00-\u9fa5]{2}/g
1121
- // 2字词汇
1122
- ];
1123
- for (const pattern of wordPatterns) {
1124
- const matches = chineseText.match(pattern) || [];
1125
- for (const word of matches) {
1126
- if (!this.stopWords.has(word) && this.isMeaningfulChineseWord(word)) {
1127
- tokens.push(word.toLowerCase());
1128
- }
1129
- }
1280
+ function cleanHtmlContent(content) {
1281
+ const cacheKey = content;
1282
+ if (htmlCleanupCache.has(cacheKey)) {
1283
+ return htmlCleanupCache.get(cacheKey);
1284
+ }
1285
+ const cleaned = content.replace(/<[^>]*>/g, " ").replace(/&[^;]+;/g, " ").replace(/\s+/g, " ").trim();
1286
+ htmlCleanupCache.set(cacheKey, cleaned);
1287
+ manageCacheSize();
1288
+ return cleaned;
1289
+ }
1290
+ function simplifiedKeywordExtraction(text, maxKeywords = 8) {
1291
+ const words = [];
1292
+ const wordCount = /* @__PURE__ */ new Map();
1293
+ const englishWords = text.match(/[a-zA-Z]{3,}/g) || [];
1294
+ for (const word of englishWords) {
1295
+ const lowerWord = word.toLowerCase();
1296
+ if (!simplifiedStopWords.has(lowerWord) && lowerWord.length >= 3) {
1297
+ words.push(lowerWord);
1298
+ wordCount.set(lowerWord, (wordCount.get(lowerWord) || 0) + 1);
1130
1299
  }
1131
- return tokens;
1132
1300
  }
1133
- /**
1134
- * 判断是否为有意义的中文词汇
1135
- */
1136
- isMeaningfulChineseWord(word) {
1137
- const meaninglessPatterns = [
1138
- /^的.*$/,
1139
- /^.*的$/,
1140
- /^了.*$/,
1141
- /^.*了$/,
1142
- /^在.*$/,
1143
- /^.*在$/,
1144
- /^是.*$/,
1145
- /^.*是$/,
1146
- /^我.*$/,
1147
- /^.*我$/,
1148
- /^有.*$/,
1149
- /^.*有$/,
1150
- /^和.*$/,
1151
- /^.*和$/,
1152
- /^就.*$/,
1153
- /^.*就$/,
1154
- /^不.*$/,
1155
- /^.*不$/,
1156
- /^人.*$/,
1157
- /^.*人$/
1158
- ];
1159
- for (const pattern of meaninglessPatterns) {
1160
- if (pattern.test(word)) {
1161
- return false;
1162
- }
1163
- }
1164
- if (/(.)\1{2,}/.test(word)) {
1165
- return false;
1301
+ const chineseWords = text.match(/[\u4e00-\u9fa5]{2,4}/g) || [];
1302
+ for (const word of chineseWords) {
1303
+ if (!simplifiedStopWords.has(word)) {
1304
+ words.push(word);
1305
+ wordCount.set(word, (wordCount.get(word) || 0) + 1);
1166
1306
  }
1167
- return true;
1168
- }
1169
- /**
1170
- * 计算逆文档频率 (IDF) - 简化版本
1171
- */
1172
- calculateInverseDocumentFrequency(term) {
1173
- const commonTerms = /* @__PURE__ */ new Set([
1174
- "\u6280\u672F",
1175
- "\u5F00\u53D1",
1176
- "\u4EE3\u7801",
1177
- "\u7CFB\u7EDF",
1178
- "\u6570\u636E",
1179
- "\u529F\u80FD",
1180
- "\u5E94\u7528",
1181
- "\u5B9E\u73B0",
1182
- "\u65B9\u6CD5",
1183
- "\u95EE\u9898",
1184
- "time",
1185
- "data",
1186
- "system",
1187
- "code",
1188
- "development",
1189
- "application",
1190
- "function",
1191
- "method",
1192
- "problem",
1193
- "solution"
1194
- ]);
1195
- if (commonTerms.has(term.toLowerCase())) {
1196
- return Math.log(1e3 / 500);
1197
- }
1198
- return Math.log(1e3 / 10);
1199
1307
  }
1200
- /**
1201
- * 提取关键词
1202
- */
1203
- extractKeywords(content, maxKeywords = 10) {
1204
- const tf = this.calculateTermFrequency(content);
1205
- const keywordScores = /* @__PURE__ */ new Map();
1206
- for (const [term, frequency] of tf) {
1207
- const idf = this.calculateInverseDocumentFrequency(term);
1208
- const tfidf = frequency * idf;
1209
- let bonus = 1;
1210
- if (content.toLowerCase().includes(term.toLowerCase()) && (content.match(new RegExp(`^${term}`, "mi")) || content.match(new RegExp(`${term}$`, "mi")))) {
1211
- bonus *= 1.5;
1212
- }
1213
- if (term.length >= 2 && term.length <= 6) {
1214
- bonus *= 1.2;
1215
- }
1216
- keywordScores.set(term, tfidf * bonus);
1217
- }
1218
- return Array.from(keywordScores.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxKeywords).map(([term]) => term);
1219
- }
1220
- };
1221
- function extractDescription(content, maxLength = 160) {
1222
- const plainText = content.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
1223
- if (plainText.length <= maxLength) return plainText;
1224
- const truncated = plainText.substring(0, maxLength);
1225
- const lastSentenceEnd = Math.max(
1226
- truncated.lastIndexOf("\u3002"),
1227
- truncated.lastIndexOf("\uFF01"),
1228
- truncated.lastIndexOf("\uFF1F"),
1229
- truncated.lastIndexOf("."),
1230
- truncated.lastIndexOf("!"),
1231
- truncated.lastIndexOf("?")
1232
- );
1233
- if (lastSentenceEnd > maxLength * 0.7) {
1234
- return truncated.substring(0, lastSentenceEnd + 1);
1235
- }
1236
- return truncated + "...";
1237
- }
1238
- function extractKeywords(content) {
1239
- const tfidfExtractor = new TFIDFKeywordExtractor();
1240
- return tfidfExtractor.extractKeywords(content, 8);
1241
- }
1242
- function generateArticleJsonLd(doc, config, pageUrl, content) {
1243
- const title = doc.Properties?.title || "\u672A\u547D\u540D\u6587\u6863";
1244
- const description = extractDescription(content);
1245
- const keywords = extractKeywords(content);
1246
- const datePublished = extractDateFromId(doc.ID);
1247
- const dateModified = formatDate(doc.Properties?.updated);
1248
- const jsonLd = {
1249
- "@context": "https://schema.org",
1250
- "@type": "Article",
1251
- headline: title,
1252
- description,
1253
- keywords: keywords.join(", "),
1254
- datePublished,
1255
- dateModified,
1256
- author: {
1257
- "@type": "Person",
1258
- name: config.sitemap?.title || "\u5D2E\u751F",
1259
- url: config.sitemap?.siteLink || ""
1260
- },
1261
- publisher: {
1262
- "@type": "Organization",
1263
- name: config.sitemap?.title || "OceanPress",
1264
- logo: {
1265
- "@type": "ImageObject",
1266
- url: `${config.sitemap?.siteLink || ""}/assets/logo.png`
1267
- }
1268
- },
1269
- mainEntityOfPage: {
1270
- "@type": "WebPage",
1271
- "@id": pageUrl
1272
- },
1273
- image: doc.Properties?.["title-img"] ? {
1274
- "@type": "ImageObject",
1275
- url: `${config.sitemap?.siteLink || ""}${doc.Properties["title-img"].replace("assets", "/assets")}`,
1276
- width: 1200,
1277
- height: 630
1278
- } : void 0,
1279
- wordCount: content.replace(/<[^>]*>/g, "").length,
1280
- articleSection: "\u6280\u672F\u6587\u6863",
1281
- inLanguage: "zh-CN"
1282
- };
1283
- return `<script type="application/ld+json">
1284
- ${JSON.stringify(jsonLd, null, 2)}
1285
- </script>`;
1308
+ return Array.from(wordCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxKeywords).map(([word]) => word);
1286
1309
  }
1287
1310
  function generateBreadcrumbJsonLd(breadcrumbs) {
1288
1311
  const itemListElement = breadcrumbs.map((crumb, index) => ({
@@ -1300,10 +1323,20 @@ function generateBreadcrumbJsonLd(breadcrumbs) {
1300
1323
  ${JSON.stringify(jsonLd, null, 2)}
1301
1324
  </script>`;
1302
1325
  }
1303
- function generateSeoMetaTags(doc, config, pageUrl, content) {
1326
+ function generateSeoContent(data) {
1327
+ const plainText = cleanHtmlContent(data.content);
1328
+ const description = extractDescriptionFromPlainText(plainText);
1329
+ const keywords = simplifiedKeywordExtraction(plainText, 8);
1330
+ const metaTags = generateSeoMetaTagsFromCache(data.doc, data.config, data.pageUrl, { plainText, description, keywords });
1331
+ let jsonLd = generateArticleJsonLdFromCache(data.doc, data.config, data.pageUrl, { plainText, description, keywords });
1332
+ if (data.breadcrumbs && data.breadcrumbs.length > 0) {
1333
+ jsonLd += "\n" + generateBreadcrumbJsonLd(data.breadcrumbs);
1334
+ }
1335
+ return { metaTags, jsonLd };
1336
+ }
1337
+ function generateSeoMetaTagsFromCache(doc, config, pageUrl, cache2) {
1304
1338
  const title = doc.Properties?.title || "\u672A\u547D\u540D\u6587\u6863";
1305
- const description = extractDescription(content);
1306
- const keywords = extractKeywords(content);
1339
+ const { description, keywords } = cache2;
1307
1340
  let metaTags = "";
1308
1341
  metaTags += ` <meta name="description" content="${description.replace(/"/g, "&quot;")}" />
1309
1342
  `;
@@ -1353,16 +1386,62 @@ function generateSeoMetaTags(doc, config, pageUrl, content) {
1353
1386
  `;
1354
1387
  return metaTags;
1355
1388
  }
1356
- function generateSeoContent(data) {
1357
- const metaTags = generateSeoMetaTags(data.doc, data.config, data.pageUrl, data.content);
1358
- let jsonLd = generateArticleJsonLd(data.doc, data.config, data.pageUrl, data.content);
1359
- if (data.breadcrumbs && data.breadcrumbs.length > 0) {
1360
- jsonLd += "\n" + generateBreadcrumbJsonLd(data.breadcrumbs);
1361
- }
1362
- return {
1363
- metaTags,
1364
- jsonLd
1389
+ function generateArticleJsonLdFromCache(doc, config, pageUrl, cache2) {
1390
+ const title = doc.Properties?.title || "\u672A\u547D\u540D\u6587\u6863";
1391
+ const { description, keywords } = cache2;
1392
+ const datePublished = extractDateFromId(doc.ID);
1393
+ const dateModified = formatDate(doc.Properties?.updated);
1394
+ const jsonLd = {
1395
+ "@context": "https://schema.org",
1396
+ "@type": "Article",
1397
+ headline: title,
1398
+ description,
1399
+ keywords: keywords.join(", "),
1400
+ datePublished,
1401
+ dateModified,
1402
+ author: {
1403
+ "@type": "Person",
1404
+ name: config.sitemap?.title || "\u5D2E\u751F",
1405
+ url: config.sitemap?.siteLink || ""
1406
+ },
1407
+ publisher: {
1408
+ "@type": "Organization",
1409
+ name: config.sitemap?.title || "OceanPress",
1410
+ logo: {
1411
+ "@type": "ImageObject",
1412
+ url: `${config.sitemap?.siteLink || ""}/assets/logo.png`
1413
+ }
1414
+ },
1415
+ mainEntityOfPage: {
1416
+ "@type": "WebPage",
1417
+ "@id": pageUrl
1418
+ },
1419
+ image: doc.Properties?.["title-img"] ? {
1420
+ "@type": "ImageObject",
1421
+ url: `${config.sitemap?.siteLink || ""}${doc.Properties["title-img"].replace("assets", "/assets")}`,
1422
+ width: 1200,
1423
+ height: 630
1424
+ } : void 0,
1425
+ wordCount: cache2.plainText.length,
1426
+ articleSection: "\u6280\u672F\u6587\u6863",
1427
+ inLanguage: "zh-CN"
1365
1428
  };
1429
+ return `<script type="application/ld+json">
1430
+ ${JSON.stringify(jsonLd, null, 2)}
1431
+ </script>`;
1432
+ }
1433
+ function extractDescriptionFromPlainText(plainText, maxLength = 160) {
1434
+ if (plainText.length <= maxLength) return plainText;
1435
+ const truncated = plainText.substring(0, maxLength);
1436
+ const lastSentenceEnd = Math.max(
1437
+ truncated.lastIndexOf("\u3002"),
1438
+ truncated.lastIndexOf("\uFF01"),
1439
+ truncated.lastIndexOf("\uFF1F"),
1440
+ truncated.lastIndexOf("."),
1441
+ truncated.lastIndexOf("!"),
1442
+ truncated.lastIndexOf("?")
1443
+ );
1444
+ return lastSentenceEnd > maxLength * 0.7 ? truncated.substring(0, lastSentenceEnd + 1) : truncated + "...";
1366
1445
  }
1367
1446
 
1368
1447
  // src/core/htmlTemplate.ts
@@ -2529,7 +2608,8 @@ function build(config, otherConfig) {
2529
2608
  return config.enableIncrementalCompilation_doc;
2530
2609
  })();
2531
2610
  effectLog.log(`=== \u5F00\u59CB\u6E32\u67D3\u6587\u6863 ===`);
2532
- yield* Effect4.forEach(Object.entries(docTree), ([path, { sy, docBlock }]) => {
2611
+ const renderStartTime = Date.now();
2612
+ yield* Effect4.all(Object.entries(docTree).map(([path, { sy, docBlock }]) => {
2533
2613
  return Effect4.gen(function* () {
2534
2614
  if (config.enableIncrementalCompilation && enableIncrementalCompilation_doc && /** 文档本身没有发生变化 */
2535
2615
  config.__skipBuilds__[docBlock.id]?.hash === docBlock.hash && /** docBlock所引用的文档也没有更新 */
@@ -2574,16 +2654,19 @@ function build(config, otherConfig) {
2574
2654
  [...renderInstance.refs.values()]
2575
2655
  )
2576
2656
  });
2577
- effectLog.log(`\u6E32\u67D3\u5B8C\u6BD5:${path}`);
2578
2657
  } catch (error) {
2579
2658
  effectLog.log(`${path} \u6E32\u67D3\u5931\u8D25:${error}`);
2580
2659
  console.log(error);
2581
2660
  }
2582
- process2(i / Doc_blocks.length);
2583
- return `\u6E32\u67D3\u5B8C\u6BD5:${path}`;
2661
+ return;
2584
2662
  });
2663
+ }), {
2664
+ /** 这里设置为 6 会比 'unbounded' 更快,猜测是并发太高会导致栈切换不过来. 在我的电脑上测试 6 是一个最佳数值 */
2665
+ concurrency: 6
2585
2666
  });
2586
- effectLog.log(`=== \u6E32\u67D3\u6587\u6863\u5B8C\u6210 ===`);
2667
+ const renderEndTime = Date.now();
2668
+ const renderDuration = ((renderEndTime - renderStartTime) / 1e3).toFixed(2);
2669
+ effectLog.log(`=== \u6587\u6863\u6E32\u67D3\u5B8C\u6BD5\uFF0C\u5171 ${Object.keys(docTree).length + 1}\u7BC7\uFF0C\u8017\u65F6: ${renderDuration}\u79D2 ===`);
2587
2670
  effectLog.log(`=== \u5F00\u59CB\u751F\u6210 sitemap.xml ===`);
2588
2671
  if (config.sitemap.enable) {
2589
2672
  fileTree["sitemap.xml"] = sitemap_xml(Doc_blocks, config.sitemap);
@@ -2646,8 +2729,11 @@ function build(config, otherConfig) {
2646
2729
  fileTree[renderDocTreeJsPath] = yield* renderDocTree();
2647
2730
  effectLog.log(`=== \u6587\u6863\u6811\u751F\u6210\u5B8C\u6210 ===`);
2648
2731
  }
2732
+ if (otherConfig?.beforeFileTree) {
2733
+ yield* Effect4.tryPromise(() => Promise.resolve(otherConfig.beforeFileTree(fileTree, effectLog)));
2734
+ }
2649
2735
  if (otherConfig?.onFileTree) {
2650
- otherConfig.onFileTree(fileTree, effectLog);
2736
+ yield* Effect4.tryPromise(() => Promise.resolve(otherConfig.onFileTree(fileTree, effectLog)));
2651
2737
  }
2652
2738
  if (config.compressedZip) {
2653
2739
  effectLog.log(`=== \u5F00\u59CB\u751F\u6210\u538B\u7F29\u5305 ===`);
@@ -2779,6 +2865,11 @@ var OceanPress = class {
2779
2865
  deployOceanPressServer_plugin(this.config)
2780
2866
  );
2781
2867
  }
2868
+ if (config.markdownMirror?.enable) {
2869
+ this.pluginCenter.registerPlugin(
2870
+ new MarkdownMirrorPlugin(config.markdownMirror)
2871
+ );
2872
+ }
2782
2873
  }
2783
2874
  build() {
2784
2875
  const build_res = this.pluginCenter.fun.build(this.config, {
@@ -2859,15 +2950,22 @@ var nodeApiDep = {
2859
2950
  // src/cli/common.ts
2860
2951
  import { Command } from "commander";
2861
2952
  var program = new Command();
2862
- program.name("OceanPress").description("\u8FD9\u662F\u4E00\u6B3E\u4ECE\u601D\u6E90\u7B14\u8BB0\u672C\u751F\u6210\u4E00\u4E2A\u9759\u6001\u7AD9\u70B9\u7684\u5DE5\u5177");
2953
+ program.name("OceanPress").description("\u8FD9\u662F\u4E00\u6B3E\u4ECE\u601D\u6E90\u7B14\u8BB0\u672C\u751F\u6210\u4E00\u4E2A\u9759\u6001\u7AD9\u70B9\u7684\u5DE5\u5177").version(package_default.version);
2863
2954
 
2864
2955
  // src/cli/deploy.ts
2865
2956
  import { Context as Context3, Effect as Effect5 } from "effect";
2866
2957
  program.command("deploy").description("\u90E8\u7F72\u7AD9\u70B9").option("-c, --config <string>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u7684\u4F4D\u7F6E").option("-h, --apiBase <string>", "OceanPress server \u5730\u5740").option("-k, --apiKey <string>", "OceanPress server Api \u5BC6\u94A5").action(async (opt) => {
2867
2958
  if (!opt.apiBase || !opt.apiKey) {
2868
- return console.error(`\u8BF7\u914D\u7F6E apiBase \u548C apiKey`);
2959
+ console.error(`\u8BF7\u914D\u7F6E apiBase \u548C apiKey`);
2960
+ throw new Error("\u8BF7\u914D\u7F6E apiBase \u548C apiKey");
2961
+ }
2962
+ let config;
2963
+ try {
2964
+ config = await readFile(opt.config, "utf-8");
2965
+ } catch (error) {
2966
+ console.error("\u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", error);
2967
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25");
2869
2968
  }
2870
- const config = await readFile(opt.config, "utf-8");
2871
2969
  const client = await createRPC("apiConsumer", {
2872
2970
  remoteCall(method, data) {
2873
2971
  let body;
@@ -2877,7 +2975,9 @@ program.command("deploy").description("\u90E8\u7F72\u7AD9\u70B9").option("-c, --
2877
2975
  content_type = "application/octet-stream";
2878
2976
  } else {
2879
2977
  body = stringify2(data);
2880
- console.log("[body]", body);
2978
+ if (process.env.NODE_ENV !== "production") {
2979
+ console.log("[body]", body);
2980
+ }
2881
2981
  content_type = "application/json";
2882
2982
  }
2883
2983
  return fetch(`${opt.apiBase}/api/${method}`, {
@@ -2893,7 +2993,7 @@ program.command("deploy").description("\u90E8\u7F72\u7AD9\u70B9").option("-c, --
2893
2993
  }).then((res) => res.json()).then((r) => {
2894
2994
  if (r.error) {
2895
2995
  console.log("[r]", r);
2896
- throw new Error();
2996
+ throw new Error(r.error.message || "API\u8BF7\u6C42\u5931\u8D25");
2897
2997
  }
2898
2998
  return r.result;
2899
2999
  });
@@ -2919,7 +3019,14 @@ ${msg}`);
2919
3019
  );
2920
3020
  const p = Effect5.provide(
2921
3021
  Effect5.gen(function* () {
2922
- yield* loadConfigFile(JSON.parse(config));
3022
+ let parsedConfig;
3023
+ try {
3024
+ parsedConfig = JSON.parse(config);
3025
+ } catch (error) {
3026
+ console.error("\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25:", error);
3027
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF");
3028
+ }
3029
+ yield* loadConfigFile(parsedConfig);
2923
3030
  const ocean_press = new OceanPress(currentConfig.value);
2924
3031
  ocean_press.pluginCenter.registerPlugin({
2925
3032
  async build_onFileTree([tree], next) {
@@ -2947,16 +3054,36 @@ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
2947
3054
  import { resolve } from "path";
2948
3055
  import { join } from "path/posix";
2949
3056
  import { Context as Context4, Effect as Effect6 } from "effect";
2950
- program.command("build").description("\u8F93\u51FA\u9759\u6001\u7AD9\u70B9\u6E90\u7801").option("-c, --config <string>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u7684\u4F4D\u7F6E").option("-o, --output <string>", "\u6307\u5B9A\u8F93\u51FA\u76EE\u5F55\u4F4D\u7F6E").action(async (opt) => {
3057
+ program.command("build").description("\u8F93\u51FA\u9759\u6001\u7AD9\u70B9\u6E90\u7801").option("-c, --config <string>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u7684\u4F4D\u7F6E").option("-o, --output <string>", "\u6307\u5B9A\u8F93\u51FA\u76EE\u5F55\u4F4D\u7F6E").option("--md", "\u542F\u7528 Markdown \u955C\u50CF\u5BFC\u51FA\u6A21\u5F0F").option("--watch", "\u542F\u7528\u76D1\u542C\u6A21\u5F0F\uFF0C\u5B9A\u65F6\u81EA\u52A8\u91CD\u65B0\u6784\u5EFA").option("--watch-interval <number>", "\u76D1\u542C\u6A21\u5F0F\u4E0B\u7684\u540C\u6B65\u95F4\u9694\uFF08\u79D2\uFF09", "60").action(async (opt) => {
2951
3058
  if (!opt.config || !opt.output) {
2952
3059
  console.log(`\u8BF7\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E\u548C\u8F93\u51FA\u76EE\u5F55\u4F4D\u7F6E`);
2953
3060
  throw new Error("\u8BF7\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E\u548C\u8F93\u51FA\u76EE\u5F55\u4F4D\u7F6E");
2954
3061
  }
2955
3062
  const config = await readFile2(opt.config, "utf-8");
2956
3063
  const filePath = resolve(opt.output);
3064
+ let parsedConfig;
3065
+ try {
3066
+ parsedConfig = JSON.parse(config);
3067
+ } catch (error) {
3068
+ console.error("\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25:", error);
3069
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF");
3070
+ }
3071
+ if (opt.md) {
3072
+ const currentProfileName = parsedConfig.__current__;
3073
+ const currentProfile = parsedConfig[currentProfileName];
3074
+ if (currentProfile) {
3075
+ currentProfile.markdownMirror = {
3076
+ enable: true,
3077
+ // --md 参数强制启用
3078
+ includeAssets: currentProfile.markdownMirror?.includeAssets ?? false
3079
+ // 默认不同步资源
3080
+ };
3081
+ console.log("\u2705 \u5DF2\u542F\u7528 Markdown \u955C\u50CF\u5BFC\u51FA\u6A21\u5F0F\n");
3082
+ }
3083
+ }
2957
3084
  await Effect6.runPromise(
2958
3085
  Effect6.provideService(
2959
- loadConfigFile(JSON.parse(config)),
3086
+ loadConfigFile(parsedConfig),
2960
3087
  EffectLocalStorageDep,
2961
3088
  nodeApiDep
2962
3089
  )
@@ -2979,35 +3106,62 @@ ${msg}`);
2979
3106
  }
2980
3107
  })
2981
3108
  );
2982
- const p = Effect6.provide(
2983
- Effect6.gen(function* () {
2984
- const ocean_press = new OceanPress(currentConfig.value);
2985
- ocean_press.pluginCenter.registerPlugin({
2986
- async build_onFileTree([tree]) {
2987
- for (const [path, data] of Object.entries(tree)) {
2988
- const fullPath = join(filePath, "./", path);
2989
- const pathArray = fullPath.split("/").slice(0, -1);
2990
- const dirPath = pathArray.join("/");
2991
- mkdir(dirPath, { recursive: true });
2992
- try {
2993
- if (typeof data === "string") {
2994
- await writeFile(fullPath, data, "utf-8");
2995
- } else {
2996
- await writeFile(fullPath, new DataView(data));
3109
+ const runBuild = async () => {
3110
+ const p = Effect6.provide(
3111
+ Effect6.gen(function* () {
3112
+ const ocean_press = new OceanPress(currentConfig.value);
3113
+ ocean_press.pluginCenter.registerPlugin({
3114
+ async build_onFileTree([tree]) {
3115
+ const dirPromises = /* @__PURE__ */ new Set();
3116
+ for (const [path, data] of Object.entries(tree)) {
3117
+ const fullPath = join(filePath, path);
3118
+ const dirPath = resolve(fullPath, "..");
3119
+ if (!dirPromises.has(dirPath)) {
3120
+ dirPromises.add(dirPath);
3121
+ await mkdir(dirPath, { recursive: true });
3122
+ }
3123
+ try {
3124
+ if (typeof data === "string") {
3125
+ await writeFile(fullPath, data, "utf-8");
3126
+ } else {
3127
+ await writeFile(fullPath, new DataView(data));
3128
+ }
3129
+ } catch (error) {
3130
+ console.error(`${fullPath} \u65E0\u6CD5\u5199\u5165:`, error);
2997
3131
  }
2998
- } catch (error) {
2999
- console.log(`${fullPath} \u65E0\u6CD5\u5199\u5165`);
3000
3132
  }
3001
3133
  }
3002
- }
3003
- });
3004
- console.log("[config.__current__]", JSON.parse(config).__current__);
3005
- console.log("[currentConfig.value.name]", currentConfig.value.name);
3006
- return yield* ocean_press.build();
3007
- }),
3008
- context
3009
- );
3010
- await Effect6.runPromise(p);
3134
+ });
3135
+ console.log("[config.__current__]", parsedConfig.__current__);
3136
+ console.log("[currentConfig.value.name]", currentConfig.value.name);
3137
+ return yield* ocean_press.build();
3138
+ }),
3139
+ context
3140
+ );
3141
+ await Effect6.runPromise(p);
3142
+ };
3143
+ if (opt.watch) {
3144
+ const intervalSeconds = parseInt(opt.watchInterval || "60", 10);
3145
+ const intervalMs = intervalSeconds * 1e3;
3146
+ console.log(`
3147
+ \u{1F504} \u76D1\u542C\u6A21\u5F0F\u5DF2\u542F\u52A8\uFF0C\u6BCF ${intervalSeconds} \u79D2\u81EA\u52A8\u6784\u5EFA\u4E00\u6B21
3148
+ `);
3149
+ await runBuild();
3150
+ setInterval(async () => {
3151
+ console.log("\n\u23F0 \u5B9A\u65F6\u4EFB\u52A1\u89E6\u53D1\uFF0C\u5F00\u59CB\u6784\u5EFA...");
3152
+ try {
3153
+ await runBuild();
3154
+ const nextTime = new Date(Date.now() + intervalMs).toLocaleTimeString();
3155
+ console.log(`
3156
+ \u23F0 \u4E0B\u6B21\u6784\u5EFA: ${nextTime}
3157
+ `);
3158
+ } catch (error) {
3159
+ console.error("\u274C \u6784\u5EFA\u5931\u8D25:", error);
3160
+ }
3161
+ }, intervalMs);
3162
+ } else {
3163
+ await runBuild();
3164
+ }
3011
3165
  });
3012
3166
 
3013
3167
  // src/cli/server.ts
@@ -3135,6 +3289,13 @@ function server(config = { port: 80, hostname: "0.0.0.0" }) {
3135
3289
 
3136
3290
  // src/cli/server.ts
3137
3291
  import { Context as Context6, Effect as Effect9 } from "effect";
3292
+ function validatePort(port) {
3293
+ const portNum = Number(port);
3294
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
3295
+ throw new Error(`\u7AEF\u53E3\u53F7\u5FC5\u987B\u5728 1-65535 \u4E4B\u95F4\uFF0C\u5F53\u524D\u503C: ${port}`);
3296
+ }
3297
+ return portNum;
3298
+ }
3138
3299
  program.command("server").description("\u542F\u52A8\u52A8\u6001\u4EE3\u7406").option("-c, --config <string>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u7684\u4F4D\u7F6E").option("-h, --host <string>", "web\u670D\u52A1\u7ED1\u5B9A\u5230\u7684\u5730\u5740", "127.0.0.1").option("-p, --port <number>", "web\u670D\u52A1\u7ED1\u5B9A\u5230\u7684\u7AEF\u53E3", "80").option(
3139
3300
  "--cache <boolean>",
3140
3301
  "\u914D\u7F6E\u4E3A true \u65F6\u5F00\u542F\u7F13\u5B58,\u9ED8\u8BA4\u4E3A false \u4E0D\u5F00\u542F\u7F13\u5B58",
@@ -3143,8 +3304,15 @@ program.command("server").description("\u542F\u52A8\u52A8\u6001\u4EE3\u7406").op
3143
3304
  async (opt) => {
3144
3305
  if (!opt.config) {
3145
3306
  console.log(`\u8BF7\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E`);
3307
+ throw new Error("\u8BF7\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E");
3308
+ }
3309
+ let config;
3310
+ try {
3311
+ config = await readFile3(opt.config, "utf-8");
3312
+ } catch (error) {
3313
+ console.error("\u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", error);
3314
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25");
3146
3315
  }
3147
- const config = await readFile3(opt.config, "utf-8");
3148
3316
  setCache(opt.cache !== "false");
3149
3317
  const context = Context6.empty().pipe(
3150
3318
  Context6.add(EffectRender, renderApiDep),
@@ -3165,11 +3333,18 @@ ${msg}`);
3165
3333
  );
3166
3334
  const p = Effect9.provide(
3167
3335
  Effect9.gen(function* () {
3168
- yield* loadConfigFile(JSON.parse(config));
3336
+ let parsedConfig;
3337
+ try {
3338
+ parsedConfig = JSON.parse(config);
3339
+ } catch (error) {
3340
+ console.error("\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25:", error);
3341
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF");
3342
+ }
3343
+ yield* loadConfigFile(parsedConfig);
3169
3344
  tempConfig.cdn.siyuanPrefix = "/notebook/";
3170
3345
  return yield* server({
3171
3346
  hostname: opt.host,
3172
- port: Number(opt.port)
3347
+ port: validatePort(opt.port)
3173
3348
  });
3174
3349
  }),
3175
3350
  context
@@ -3179,5 +3354,13 @@ ${msg}`);
3179
3354
  );
3180
3355
 
3181
3356
  // src/cli.ts
3357
+ process.on("uncaughtException", (error) => {
3358
+ console.error("\u672A\u6355\u83B7\u7684\u5F02\u5E38:", error);
3359
+ process.exit(1);
3360
+ });
3361
+ process.on("unhandledRejection", (reason, promise) => {
3362
+ console.error("\u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD:", reason);
3363
+ process.exit(1);
3364
+ });
3182
3365
  program.parse(process.argv);
3183
3366
  //# sourceMappingURL=cli.js.map