pug-site-core 2.0.22 → 3.0.1

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/lib/generate.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import fse from "fs-extra";
2
2
  import pug from "pug";
3
+ import pugLexer from "pug-lexer";
4
+ import pugParser from "pug-parser";
5
+ import pugWalk from "pug-walk";
3
6
  import {
4
7
  getPagesPugFilePathArr,
5
8
  getCompilePugFilter,
@@ -13,8 +16,10 @@ import _ from "lodash";
13
16
  import async from "async";
14
17
  import UglifyJS from "uglify-js";
15
18
  import { paths } from "./paths.js";
19
+ import path from "path";
16
20
 
17
21
  const { config } = await import(paths.config);
22
+
18
23
  /**
19
24
  * 将pages目录下的pug模板编译为JS函数
20
25
  * @param {string} pugPath - 指定编译的pug文件路径(相对于/template/pages)
@@ -51,7 +56,11 @@ export async function compilePagesPugToFn(pugPath) {
51
56
  10, // 限制并发数为10
52
57
  async (fileName) => {
53
58
  const filePath = paths.resolveRoot(paths.template.pages, fileName);
54
- const funName = fileName.split(pathSymbol).join("_").slice(0, -4).replace(/[-]/g, "_");
59
+ const funName = fileName
60
+ .split(pathSymbol)
61
+ .join("_")
62
+ .slice(0, -4)
63
+ .replace(/[-]/g, "_");
55
64
 
56
65
  // 读取并编译pug文件
57
66
  const pugValue = await fse.readFile(filePath, "utf8");
@@ -141,7 +150,9 @@ export async function generateGetDataFn() {
141
150
  // 为每个页面注入数据获取函数
142
151
  await async.each(pagesPugFilePathArr, async (fileName) => {
143
152
  const funName =
144
- "get_" + fileName.split(pathSymbol).join("_").slice(0, -4).replace(/[-]/g, "_") + "_data";
153
+ "get_" +
154
+ fileName.split(pathSymbol).join("_").slice(0, -4).replace(/[-]/g, "_") +
155
+ "_data";
145
156
 
146
157
  // 使用正则表达式检查特定的数据获取函数是否存在
147
158
  const dataFnRegex = new RegExp(
@@ -241,7 +252,7 @@ export async function fetchDataToJsonFile(args) {
241
252
  // 创建一个全局任务队列控制整体并发数
242
253
  const queue = async.queue(async (task) => {
243
254
  await task();
244
- }, fetchDataConcurrencyLimit || 12);
255
+ }, fetchDataConcurrencyLimit || 3);
245
256
 
246
257
  // 收集所有需要执行的任务
247
258
  const allTasks = [];
@@ -263,6 +274,8 @@ export async function fetchDataToJsonFile(args) {
263
274
  allTasks.push(async () => {
264
275
  console.log(language, commonFuncName, "开始写入json文件");
265
276
  const commonData = await getData[commonFuncName](language);
277
+ let languageData = (await import(paths.languageData)).default[language];
278
+ commonData.lang = _.merge(commonData.lang, languageData);
266
279
  await fse.outputJSON(
267
280
  paths.resolveRoot("jsonData", language, "_common.json"),
268
281
  commonData
@@ -327,7 +340,9 @@ export async function fetchDataToJsonFile(args) {
327
340
  // 处理模板页面数据
328
341
  for (const fileName of pugFilePathList) {
329
342
  let funName =
330
- "get_" + fileName.split(pathSymbol).join("_").slice(0, -4).replace(/[-]/g, "_") + "_data";
343
+ "get_" +
344
+ fileName.split(pathSymbol).join("_").slice(0, -4).replace(/[-]/g, "_") +
345
+ "_data";
331
346
 
332
347
  let jsonFilePath = fileName.slice(0, -4).split(pathSymbol);
333
348
  if (!getData[funName] || typeof getData[funName] !== "function") {
@@ -474,244 +489,758 @@ export async function buildFn() {
474
489
  console.log("打包完成花费:", (Date.now() - starTime) / 1000, "s");
475
490
  }
476
491
 
477
- //html文件打包 不维护了
492
+ //html文件打包
478
493
  export async function buildStatic() {
479
494
  let jsonDataPath = paths.resolveRoot("jsonData");
480
495
 
481
- if (!fse.pathExistsSync(jsonDataPath)) {
482
- return Promise.reject(
483
- new Error(jsonDataPath + "目录不存在请先执行npm run getData生成数据!")
484
- );
485
- }
486
496
  console.log("开始打包...");
487
497
  let starTime = Date.now();
488
498
  let distOutputPath = paths.resolveRoot(config.staticOutput);
489
- await fse.remove(distOutputPath);
490
- await sleep(0);
491
- await fse.copy(paths.public, distOutputPath);
492
499
 
493
- await fse.copy(
494
- paths.template.static,
495
- paths.resolveRoot(distOutputPath, "static"),
496
- {
497
- filter: (src, dest) => {
498
- //根目录必须要返回true
499
- if (src.endsWith("static")) {
500
- return true;
500
+ /**
501
+ * 构建HTML文件路径
502
+ * @param {string} lang - 语言代码
503
+ * @param {string} outputPath - 输出路径
504
+ * @param {string} fileName - 文件名(可选)
505
+ * @returns {string} 完整的HTML文件路径
506
+ */
507
+ function buildHtmlPath(lang, outputPath, fileName = null) {
508
+ let htmlPath
509
+ try {
510
+ if (fileName) {
511
+ htmlPath = paths.resolveRoot(
512
+ distOutputPath,
513
+ lang,
514
+ outputPath.replace(
515
+ outputPath.split("/").pop().replace(/\..*$/, ""),
516
+ fileName.replace(/\..*$/, "")
517
+ )
518
+ );
519
+ } else {
520
+ htmlPath = paths.resolveRoot(distOutputPath, lang, outputPath);
521
+ }
522
+ console.log(htmlPath);
523
+ return htmlPath;
524
+ } catch (error) {
525
+ throw new Error(`构建HTML路径失败 [lang: ${lang}, outputPath: ${outputPath}, fileName: ${fileName}]: ${error.message}`);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * 生成HTML内容
531
+ */
532
+ function generateHtml(pagesPugToFn, funName, data, pagePath, commonData) {
533
+ try {
534
+ if (!pagesPugToFn[funName]) {
535
+ throw new Error(`模板函数 ${funName} 不存在`);
536
+ }
537
+
538
+ return pagesPugToFn[funName]({
539
+ data,
540
+ _pagePath: pagePath,
541
+ common: commonData,
542
+ });
543
+ } catch (error) {
544
+ throw new Error(`生成HTML失败 [funName: ${funName}, pagePath: ${pagePath}]: ${error.message}`);
545
+ }
546
+ }
547
+
548
+ /**
549
+ * 处理数组数据的HTML输出
550
+ */
551
+ async function processArrayData(pagesPugToFn, data, obj, funName, pagePath, commonData, lang) {
552
+ try {
553
+ const name = obj.outPutHtmlPath.split("/").pop().replace(/\..*$/, "");
554
+ const regex = /^\[.+\]$/;
555
+
556
+ if (regex.test(name)) {
557
+ const property = name.slice(1, -1);
558
+
559
+ await async.eachLimit(data, 12, async (dataItem, index) => {
560
+ try {
561
+ const fileName = dataItem[property];
562
+ if (!fileName) {
563
+ throw new Error(
564
+ `数据项索引 ${index} 中缺少属性 ${property} 或值为空`
565
+ );
566
+ }
567
+
568
+ const htmlPath = buildHtmlPath(lang, obj.outPutHtmlPath, fileName);
569
+ const html = generateHtml(pagesPugToFn, funName, dataItem, pagePath, commonData);
570
+ await fse.outputFile(htmlPath, html);
571
+ } catch (error) {
572
+ throw new Error(`处理数组数据项失败 [index: ${index}, property: ${property}]: ${error.message}`);
573
+ }
574
+ });
575
+ } else {
576
+ const htmlPath = buildHtmlPath(lang, obj.outPutHtmlPath);
577
+ const html = generateHtml(pagesPugToFn, funName, data, pagePath, commonData);
578
+ await fse.outputFile(htmlPath, html);
579
+ }
580
+ } catch (error) {
581
+ throw new Error(`处理数组数据失败 [lang: ${lang}, getDataFn: ${obj.getDataFn}]: ${error.message}`);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * 处理对象数据的HTML输出
587
+ */
588
+ async function processObjectData(pagesPugToFn, data, obj, funName, pagePath, commonData, lang) {
589
+ try {
590
+ const htmlPath = buildHtmlPath(lang, obj.outPutHtmlPath);
591
+ const html = generateHtml(pagesPugToFn, funName, data, pagePath, commonData);
592
+ await fse.outputFile(htmlPath, html);
593
+ } catch (error) {
594
+ throw new Error(`处理对象数据失败 [lang: ${lang}, getDataFn: ${obj.getDataFn}]: ${error.message}`);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * 处理自定义HTML构建
600
+ */
601
+ async function processCustomBuildHtml(pagesPugToFn, lang, getData, commonData, dealWithEndFunName) {
602
+ if (!config.customBuildHtml?.length) return;
603
+
604
+ for (const obj of config.customBuildHtml) {
605
+ try {
606
+ // 检查语言过滤
607
+ if (obj.includeLang?.length > 0 && !obj.includeLang.includes(lang)) {
608
+ continue;
501
609
  }
502
- if (config.buildStaticDirArr && config.buildStaticDirArr.length > 0) {
503
- return !!config.buildStaticDirArr.find((item) => {
504
- return src.startsWith(paths.resolveRoot(paths.template.static, item));
505
- });
610
+
611
+ // 获取数据
612
+ if (!getData[obj.getDataFn] || typeof getData[obj.getDataFn] !== 'function') {
613
+ throw new Error(`数据获取函数 ${obj.getDataFn} 不存在或不是函数`);
506
614
  }
507
- return true;
508
- },
509
- }
510
- );
511
615
 
512
- await compilePagesPugToFn();
513
- let PagesPugToFn = await import(paths.pagesPugFn);
514
- const getData = await import(paths.getData);
616
+ const data = await getData[obj.getDataFn](lang);
617
+
618
+ // 处理路径和函数名
619
+ obj.inputPugPath = obj.inputPugPath.replace(/^\//, "");
620
+ const funName = obj.inputPugPath.split("/").join("_").slice(0, -4);
621
+ dealWithEndFunName.set(funName, 1);
622
+ const pagePath = obj.inputPugPath.replaceAll("/", pathSymbol);
515
623
 
516
- const fileMapTable = config.fileMapTable;
517
- await async.each(config.languageList, async (lang) => {
518
- let langDataPath = paths.resolveRoot(jsonDataPath, lang);
519
- if (!fse.pathExistsSync(langDataPath)) {
520
- console.log(
521
- `注意配置了${lang}语言但${langDataPath}中没有生成${lang}语言的数据!`
522
- );
523
- return;
624
+ // 根据数据类型处理
625
+ if (Array.isArray(data)) {
626
+ await processArrayData(pagesPugToFn, data, obj, funName, pagePath, commonData, lang);
627
+ } else if (typeof data === "object" && data !== null) {
628
+ await processObjectData(pagesPugToFn, data, obj, funName, pagePath, commonData, lang);
629
+ } else {
630
+ throw new Error(`数据类型不支持: ${typeof data}`);
631
+ }
632
+ } catch (error) {
633
+ throw new Error(`处理自定义HTML构建失败 [lang: ${lang}, config: ${JSON.stringify(obj)}]: ${error.message}`);
634
+ }
524
635
  }
525
- let commonData = await fse.readJSON(
526
- paths.resolveRoot("jsonData", lang, "_common.json")
527
- );
528
- commonData = _.merge(commonData, config.commonData);
529
-
530
- if (fileMapTable && Array.isArray(fileMapTable)) {
531
- await async.each(fileMapTable, async (obj) => {
532
- if (
533
- obj.pugPath &&
534
- obj.getDataFn &&
535
- obj.outPutPath &&
536
- obj.outPutPath.endsWith(".html")
537
- ) {
538
- if (
539
- obj.languageList &&
540
- obj.languageList.length > 0 &&
541
- !obj.languageList.includes(lang)
542
- ) {
636
+ }
637
+
638
+ /**
639
+ * 处理页面JSON文件
640
+ */
641
+ async function processPageJsonFiles(pagesPugToFn, lang, langDataPath, commonData, dealWithEndFunName) {
642
+ try {
643
+ const pagesAllJsonFileName = (
644
+ await fse.readdir(langDataPath, { recursive: true })
645
+ ).filter((fileName) => fileName.endsWith(".json"));
646
+
647
+ await async.eachLimit(pagesAllJsonFileName, 12, async (jsonFileName) => {
648
+ try {
649
+ const data = await fse.readJSON(paths.resolveRoot(langDataPath, jsonFileName));
650
+ const pugTemplate = data._template;
651
+
652
+ if (!pugTemplate) {
653
+ return;
654
+ }
655
+
656
+ const funName = pugTemplate.split(pathSymbol).join("_").slice(0, -4);
657
+
658
+ if (dealWithEndFunName.has(funName)) {
543
659
  return;
544
660
  }
545
- let langPrefix =
546
- obj.languageList && obj.languageList.length > 0 ? lang : "";
547
- let pugPathPreArr = [""];
548
- if (obj.deviceList && obj.deviceList.length > 0) {
549
- pugPathPreArr = obj.deviceList;
661
+
662
+ const html = generateHtml(pagesPugToFn, funName, data, pugTemplate, commonData);
663
+
664
+ // 构建输出路径
665
+ let finalPugTemplate = pugTemplate;
666
+ if (data.page_name) {
667
+ finalPugTemplate =
668
+ pugTemplate.split(pathSymbol).slice(0, -1).join(pathSymbol) +
669
+ pathSymbol +
670
+ data.page_name;
550
671
  }
551
- await async.each(pugPathPreArr, async (devicePrefix) => {
552
- let pugPath = paths.resolveRoot(
553
- paths.template.pages,
554
- langPrefix,
555
- devicePrefix,
556
- obj.pugPath.split("/").join(pathSymbol)
557
- );
558
- if (!fse.pathExistsSync(pugPath)) {
559
- return Promise.reject(new Error(pugPath + "模版路径不存在"));
672
+
673
+ const htmlPath = paths.resolveRoot(
674
+ distOutputPath,
675
+ lang,
676
+ finalPugTemplate.replace(/\..*$/, ".html")
677
+ );
678
+
679
+ await fse.outputFile(htmlPath, html);
680
+ } catch (error) {
681
+ throw new Error(`处理JSON文件失败 [file: ${jsonFileName}]: ${error.message}`);
682
+ }
683
+ });
684
+ } catch (error) {
685
+ throw new Error(`处理页面JSON文件失败 [lang: ${lang}]: ${error.message}`);
686
+ }
687
+ }
688
+
689
+ try {
690
+ const getData = await import(paths.getData);
691
+
692
+ // 清理并准备输出目录
693
+ await fse.remove(distOutputPath);
694
+ await sleep(0);
695
+ await fse.copy(paths.public, distOutputPath);
696
+
697
+ // 复制静态资源
698
+ await fse.copy(
699
+ paths.template.static,
700
+ paths.resolveRoot(distOutputPath, "static"),
701
+ {
702
+ filter: (src, dest) => {
703
+ //根目录必须要返回true
704
+ if (src.endsWith("static")) {
705
+ return true;
706
+ }
707
+ if (config.buildStaticDirArr && config.buildStaticDirArr.length > 0) {
708
+ return !!config.buildStaticDirArr.find((item) => {
709
+ return src.startsWith(paths.resolveRoot(paths.template.static, item));
710
+ });
711
+ }
712
+ return true;
713
+ },
714
+ }
715
+ );
716
+
717
+ // 编译PUG模板
718
+ await compilePagesPugToFn();
719
+ const PagesPugToFn = await import(paths.pagesPugFn);
720
+
721
+ // 处理每种语言
722
+ await async.each(config.languageList, async (lang) => {
723
+ try {
724
+ console.log("开始处理语言:", lang);
725
+ const langDataPath = paths.resolveRoot(jsonDataPath, lang);
726
+
727
+ // 读取公共数据
728
+ let commonData;
729
+ try {
730
+ commonData = await fse.readJSON(
731
+ paths.resolveRoot("jsonData", lang, "_common.json")
732
+ );
733
+ commonData = _.merge(commonData, config.commonData);
734
+ } catch (error) {
735
+ throw new Error(`读取公共数据失败: ${error.message}`);
736
+ }
737
+
738
+ const dealWithEndFunName = new Map();
739
+
740
+ // 处理自定义HTML构建
741
+ await processCustomBuildHtml(PagesPugToFn, lang, getData, commonData, dealWithEndFunName);
742
+
743
+ // 处理页面JSON文件
744
+ await processPageJsonFiles(PagesPugToFn, lang, langDataPath, commonData, dealWithEndFunName);
745
+
746
+ } catch (error) {
747
+ throw new Error(`处理语言 ${lang} 失败: ${error.message}`);
748
+ }
749
+ });
750
+
751
+ // 混淆JavaScript文件(如果配置了)
752
+ if (config.obfuscateJavaScript) {
753
+ console.log("开始混淆js文件...");
754
+ const startTime = Date.now();
755
+ try {
756
+ await obfuscateJavaScript(paths.resolveRoot(distOutputPath, "static"));
757
+ const costTime = (Date.now() - startTime) / 1000;
758
+ console.log("混淆js文件耗时:", costTime, "s");
759
+ } catch (error) {
760
+ throw new Error(`混淆JavaScript文件失败: ${error.message}`);
761
+ }
762
+ }
763
+
764
+ console.log("打包完成花费:", (Date.now() - starTime) / 1000, "s");
765
+
766
+ } catch (error) {
767
+ console.error("HTML文件打包失败:", error);
768
+ throw error;
769
+ }
770
+ }
771
+
772
+ /**
773
+ * 创建调试模板目录
774
+ * 复制template目录到template-debug并为所有pug文件添加调试属性
775
+ * @returns {Promise<void>}
776
+ */
777
+ export async function createDebugTemplate() {
778
+ try {
779
+ console.log("开始创建调试模板目录...");
780
+
781
+ const sourceDir = paths.template.root;
782
+ const targetDir = paths.template.debug;
783
+
784
+ // 删除已存在的调试目录
785
+ if (await fse.pathExists(targetDir)) {
786
+ await fse.remove(targetDir);
787
+ console.log("已删除现有的调试目录");
788
+ }
789
+
790
+ // 复制整个 template 目录到 template-debug
791
+ await fse.copy(sourceDir, targetDir);
792
+ console.log(`已复制 ${sourceDir} 到 ${targetDir}`);
793
+
794
+ let languageData = (await import(paths.languageData)).default.us;
795
+
796
+ // 缓存语言路径检查结果,避免重复计算
797
+ const langPathCache = new Map();
798
+
799
+ /**
800
+ * 检查属性路径是否在languageData的us对象中存在
801
+ * @param {string} langPath - 属性路径,例如 "Store.about"
802
+ * @returns {boolean} 是否存在该属性
803
+ */
804
+ function checkLangPathExists(langPath) {
805
+ // 使用缓存提升性能
806
+ if (langPathCache.has(langPath)) {
807
+ return langPathCache.get(langPath);
808
+ }
809
+
810
+ try {
811
+ const keys = langPath.split(".");
812
+ let current = languageData;
813
+
814
+ for (const key of keys) {
815
+ if (current && typeof current === "object" && key in current) {
816
+ current = current[key];
817
+ } else {
818
+ langPathCache.set(langPath, false);
819
+ return false;
820
+ }
821
+ }
822
+
823
+ const isValid = typeof current === "string";
824
+ langPathCache.set(langPath, isValid);
825
+ return isValid;
826
+ } catch (error) {
827
+ langPathCache.set(langPath, false);
828
+ return false;
829
+ }
830
+ }
831
+
832
+ /**
833
+ * 检查是否是动态内容
834
+ * @param {string} code - 代码内容
835
+ * @returns {boolean} 是否包含动态数据
836
+ */
837
+ function isDynamicContent(code) {
838
+ // 更精确的动态内容检测
839
+ const dynamicPatterns = [
840
+ /\b(Math|Date|JSON|parseInt|parseFloat|Number|String|Array|Object)\b/,
841
+ /\b(data|item|index|info|common\.siteConfig)\b/,
842
+ /[+\-*/%]/,
843
+ /\brandom\b|\bfloor\b|\bceil\b|\bround\b/,
844
+ /\.\w+\(/, // 方法调用
845
+ /\$\{[^}]*[+\-*/%.][^}]*\}/, // 模板字符串中的计算
846
+ ];
847
+
848
+ return dynamicPatterns.some((pattern) => pattern.test(code));
849
+ }
850
+
851
+ /**
852
+ * 提取Pug节点中的common.lang引用
853
+ * @param {string} code - 代码内容
854
+ * @returns {string|null} 语言路径,如果不是common.lang引用则返回null
855
+ */
856
+ function extractLangPath(code) {
857
+ // 匹配 common.lang.xxx
858
+ const langMatch = code.match(/common\.lang\.(.+)/);
859
+ if (langMatch) {
860
+ return langMatch[1];
861
+ }
862
+
863
+ // 匹配 ${common.lang.xxx}
864
+ const templateLangMatch = code.match(/\$\{common\.lang\.([^}]+)\}/);
865
+ if (templateLangMatch) {
866
+ return templateLangMatch[1];
867
+ }
868
+
869
+ // 匹配带反引号的模板字符串 `${common.lang.xxx}`
870
+ const backquoteLangMatch = code.match(/`.*\$\{common\.lang\.([^}]+)\}.*`/);
871
+ if (backquoteLangMatch) {
872
+ return backquoteLangMatch[1];
873
+ }
874
+
875
+ return null;
876
+ }
877
+
878
+ /**
879
+ * 使用 pug-parser 和 pug-walk 分析 pug 文件中的标签
880
+ * @param {string} pugContent - pug 文件内容
881
+ * @param {string} filename - 文件名
882
+ * @returns {Map} 行号到标签信息的映射
883
+ */
884
+ function analyzeTagsFromPug(pugContent, filename) {
885
+ try {
886
+ const tokens = pugLexer(pugContent, { filename });
887
+ const ast = pugParser(tokens, { filename, src: pugContent });
888
+
889
+ const lineTagMap = new Map();
890
+
891
+ pugWalk(ast, function before(node) {
892
+ if (node.type === "Tag" && node.line) {
893
+ const tagInfo = {
894
+ tagName: node.name,
895
+ line: node.line,
896
+ isEditable: false,
897
+ editableValue: "true",
898
+ textContent: "",
899
+ langPath: "",
900
+ hasStaticText: false,
901
+ hasLangText: false,
902
+ };
903
+
904
+ // 分析标签的子节点来确定文本内容
905
+ if (node.block && node.block.nodes) {
906
+ for (const child of node.block.nodes) {
907
+ // 处理Text节点(静态文本)
908
+ if (child.type === "Text" && child.val && child.val.trim()) {
909
+ // 检查是否在同一行还有其他动态内容
910
+ const hasOtherDynamicNodes = node.block.nodes.some(
911
+ (sibling) =>
912
+ sibling !== child &&
913
+ sibling.line === child.line &&
914
+ (sibling.type === "Code" ||
915
+ sibling.type === "InterpolatedTag") &&
916
+ sibling.val &&
917
+ isDynamicContent(sibling.val)
918
+ );
919
+
920
+ if (!hasOtherDynamicNodes) {
921
+ tagInfo.hasStaticText = true;
922
+ tagInfo.textContent = child.val.trim();
923
+ tagInfo.isEditable = true;
924
+ }
925
+ }
926
+
927
+ // 处理Code节点(插值表达式和代码块)
928
+ if (child.type === "Code" && child.val) {
929
+ const langPath = extractLangPath(child.val);
930
+ if (langPath && !isDynamicContent(child.val)) {
931
+ if (checkLangPathExists(langPath)) {
932
+ tagInfo.hasLangText = true;
933
+ tagInfo.langPath = langPath;
934
+ tagInfo.isEditable = true;
935
+ // 转换为 [us][key1][key2] 格式
936
+ const pathParts = langPath.split(".");
937
+ tagInfo.editableValue = `us,${pathParts.join(",")}`;
938
+ }
939
+ }
940
+ }
941
+ }
560
942
  }
561
- let dataFn = getData[obj.getDataFn];
562
- if (!dataFn || typeof dataFn !== "function") {
563
- return Promise.reject(new Error(obj.getDataFn + "获取数据函数不存在"));
943
+
944
+ // 检查节点本身是否有代码内容(对于!=语法)
945
+ if (node.code && node.code.val) {
946
+ const langPath = extractLangPath(node.code.val);
947
+ if (langPath && !isDynamicContent(node.code.val)) {
948
+ if (checkLangPathExists(langPath)) {
949
+ tagInfo.hasLangText = true;
950
+ tagInfo.langPath = langPath;
951
+ tagInfo.isEditable = true;
952
+ // 转换为 us,key1,key2 格式
953
+ const pathParts = langPath.split(".");
954
+ tagInfo.editableValue = `us,${pathParts.join(",")}`;
955
+ }
956
+ }
564
957
  }
565
- let data = await dataFn(lang);
566
- if (!data) {
567
- return Promise.reject(new Error(dataFn + "获取的数据为null!"));
958
+
959
+ // 将标签信息添加到行映射中
960
+ if (!lineTagMap.has(node.line)) {
961
+ lineTagMap.set(node.line, []);
568
962
  }
569
- let outPutPath = obj.outPutPath.split("/").join(pathSymbol);
570
- let htmlPath;
571
- let html;
572
- if (Array.isArray(data)) {
573
- let name = outPutPath.split(pathSymbol).pop().replace(/\..*$/, "");
574
- const regex = /^\[.+\]$/;
575
- if (regex.test(name)) {
576
- let property = name.slice(1, -1);
577
- for (let index = 0; index < data.length; index++) {
578
- const dataItem = data[index];
579
- let fileName = dataItem[property];
580
- if (
581
- fileName === null ||
582
- fileName === undefined ||
583
- fileName === ""
584
- ) {
585
- return Promise.reject(
586
- new Error(
587
- dataFn +
588
- "获取的数据中期望以" +
589
- property +
590
- `命名但是${index}下标中对象${property}属性为:${fileName}`
591
- )
592
- );
593
- }
594
- htmlPath = paths.resolveRoot.join(
595
- distOutputPath,
596
- lang,
597
- devicePrefix,
598
- outPutPath.replace(name, fileName)
963
+ lineTagMap.get(node.line).push(tagInfo);
964
+ }
965
+ });
966
+
967
+ return lineTagMap;
968
+ } catch (error) {
969
+ console.error(`分析 pug 文件 ${filename} 时出错:`, error);
970
+ return new Map();
971
+ }
972
+ }
973
+
974
+ /**
975
+ * pug 文件内容添加调试属性
976
+ * @param {string} pugContent - pug 文件内容
977
+ * @param {string} relativePath - 文件相对路径
978
+ * @param {string} filename - 文件名
979
+ * @returns {string} 处理后的 pug 内容
980
+ */
981
+ function addDebugAttributesToPug(pugContent, relativePath, filename) {
982
+ try {
983
+ const lines = pugContent.split("\n");
984
+ const lineTagMap = analyzeTagsFromPug(pugContent, filename);
985
+
986
+ // 定义不需要添加调试属性的标签
987
+ const excludeTags = new Set([
988
+ "head",
989
+ "title",
990
+ "meta",
991
+ "link",
992
+ "html",
993
+ "style",
994
+ "script",
995
+ "base",
996
+ "noscript",
997
+ "template",
998
+ ]);
999
+
1000
+ // 处理每一行
1001
+ const processedLines = lines.map((line, index) => {
1002
+ const lineNumber = index + 1;
1003
+ const tagsInLine = lineTagMap.get(lineNumber);
1004
+
1005
+ if (!tagsInLine || tagsInLine.length === 0) {
1006
+ return line;
1007
+ }
1008
+
1009
+ // 过滤掉不需要处理的标签
1010
+ const visibleTags = tagsInLine.filter(
1011
+ (tag) => !excludeTags.has(tag.tagName.toLowerCase())
1012
+ );
1013
+
1014
+ if (visibleTags.length === 0) {
1015
+ return line;
1016
+ }
1017
+
1018
+ // 对于每个标签,添加调试属性
1019
+ let processedLine = line;
1020
+
1021
+ // 只处理第一个标签,避免处理插值表达式中的内容
1022
+ const mainTag = visibleTags[0];
1023
+
1024
+ if (mainTag) {
1025
+ // 构建调试属性
1026
+ const debugAttrs = [
1027
+ `data-debug-file="${relativePath}"`,
1028
+ `data-debug-line="${lineNumber}"`,
1029
+ ];
1030
+
1031
+ // 只有可编辑的元素才添加 data-debug-editable 属性
1032
+ if (mainTag.isEditable) {
1033
+ debugAttrs.push(`data-debug-editable="${mainTag.editableValue}"`);
1034
+ }
1035
+
1036
+ const debugAttrsString = debugAttrs.join(", ");
1037
+
1038
+ // 简化的标签处理逻辑
1039
+ const trimmedLine = line.trim();
1040
+
1041
+ // 处理隐式 div(.class 或 #id 语法)
1042
+ if (trimmedLine.startsWith(".") || trimmedLine.startsWith("#")) {
1043
+ const indent = line.match(/^\s*/)[0];
1044
+
1045
+ // 找到选择器结束的位置
1046
+ let selectorEndIndex = 1;
1047
+ while (
1048
+ selectorEndIndex < trimmedLine.length &&
1049
+ /[\w-]/.test(trimmedLine[selectorEndIndex])
1050
+ ) {
1051
+ selectorEndIndex++;
1052
+ }
1053
+
1054
+ // 检查是否已经有属性括号
1055
+ const afterSelector = trimmedLine.substring(selectorEndIndex);
1056
+ const parenIndex = afterSelector.indexOf("(");
1057
+
1058
+ if (parenIndex !== -1) {
1059
+ // 已经有属性括号
1060
+ const beforeParen = trimmedLine.substring(
1061
+ 0,
1062
+ selectorEndIndex + parenIndex + 1
1063
+ );
1064
+ const afterFirstParen = trimmedLine.substring(
1065
+ selectorEndIndex + parenIndex + 1
1066
+ );
1067
+ const lastParenIndex = afterFirstParen.lastIndexOf(")");
1068
+
1069
+ if (lastParenIndex !== -1) {
1070
+ const existingAttrs = afterFirstParen.substring(0, lastParenIndex);
1071
+ const afterLastParen = afterFirstParen.substring(lastParenIndex);
1072
+
1073
+ const separator = existingAttrs.trim() ? ", " : "";
1074
+ processedLine = `${indent}${beforeParen}${existingAttrs}${separator}${debugAttrsString}${afterLastParen}`;
1075
+ }
1076
+ } else {
1077
+ // 没有属性括号,在选择器后添加
1078
+ const selector = trimmedLine.substring(0, selectorEndIndex);
1079
+ const afterAttrs = trimmedLine.substring(selectorEndIndex);
1080
+
1081
+ processedLine = `${indent}${selector}(${debugAttrsString})${afterAttrs}`;
1082
+ }
1083
+ }
1084
+ // 处理普通标签
1085
+ else if (trimmedLine.startsWith(mainTag.tagName)) {
1086
+ const indent = line.match(/^\s*/)[0];
1087
+
1088
+ // 找到标签名和修饰符的结束位置
1089
+ const tagEndIndex = findTagEnd(mainTag.tagName, trimmedLine);
1090
+ const afterTag = trimmedLine.substring(tagEndIndex);
1091
+
1092
+ if (afterTag.startsWith("(")) {
1093
+ // 已经有属性括号,需要找到正确的结束位置
1094
+ const parenEnd = findMatchingParen(trimmedLine, tagEndIndex);
1095
+
1096
+ if (parenEnd !== -1) {
1097
+ const tagWithModifiers = trimmedLine.substring(0, tagEndIndex);
1098
+ const existingAttrs = trimmedLine.substring(
1099
+ tagEndIndex + 1,
1100
+ parenEnd
599
1101
  );
600
- html = pug.compileFile(pugPath, {
601
- basedir: paths.template.root,
602
- compileDebug: true,
603
- filters: getCompilePugFilter(),
604
- })({
605
- data: dataItem,
606
- _pagePath: obj.pugPath,
607
- common: commonData,
608
- });
609
- fse.ensureFileSync(htmlPath);
610
- await fse.writeFile(htmlPath, html);
1102
+ const remaining = trimmedLine.substring(parenEnd + 1);
1103
+
1104
+ const separator = existingAttrs.trim() ? ", " : "";
1105
+ processedLine = `${indent}${tagWithModifiers}(${existingAttrs}${separator}${debugAttrsString})${remaining}`;
611
1106
  }
612
1107
  } else {
613
- htmlPath = paths.resolveRoot(
614
- paths.template.root,
615
- lang,
616
- devicePrefix,
617
- outPutPath
618
- );
619
- html = pug.compileFile(pugPath, {
620
- basedir: paths.template.root,
621
- compileDebug: true,
622
- filters: getCompilePugFilter(),
623
- })({
624
- data,
625
- _pagePath: obj.pugPath,
626
- common: commonData,
627
- });
628
- fse.ensureFileSync(htmlPath);
629
- await fse.writeFile(htmlPath, html);
1108
+ // 没有属性括号,添加新的属性括号
1109
+ const tagWithModifiers = trimmedLine.substring(0, tagEndIndex);
1110
+ const remaining = trimmedLine.substring(tagEndIndex);
1111
+
1112
+ processedLine = `${indent}${tagWithModifiers}(${debugAttrsString})${remaining}`;
630
1113
  }
631
- } else if (typeof data === "object") {
632
- htmlPath = paths.resolveRoot(
633
- distOutputPath,
634
- lang,
635
- devicePrefix,
636
- outPutPath
637
- );
638
- html = pug.compileFile(pugPath, {
639
- basedir: paths.template.root,
640
- compileDebug: true,
641
- filters: getCompilePugFilter(),
642
- })({
643
- data,
644
- _pagePath: obj.pugPath,
645
- common: commonData,
646
- });
647
- fse.ensureFileSync(htmlPath);
648
- await fse.writeFile(htmlPath, html);
649
1114
  }
650
- });
651
- }
652
- });
653
- }
1115
+ }
1116
+
1117
+ return processedLine;
1118
+ });
654
1119
 
655
- let pagesAllJsonFileName = (
656
- await fse.readdir(langDataPath, {
657
- recursive: true,
658
- })
659
- ).filter((fileName) => fileName.endsWith(".json"));
660
- await async.eachLimit(pagesAllJsonFileName, 64, async (jsonFileName) => {
661
- let data = await fse.readJSON(paths.resolveRoot(langDataPath, jsonFileName));
662
- let pugTemplateArr = data._template;
663
- if (!pugTemplateArr) {
664
- return;
1120
+ return processedLines.join("\n");
1121
+ } catch (error) {
1122
+ console.error(`处理 pug 文件 ${filename} 时出错:`, error);
1123
+ return pugContent;
665
1124
  }
666
- let flag = false;
1125
+ }
667
1126
 
668
- let curLangPugTem = data._template.find((item) => {
669
- let lang2 = item.split(pathSymbol)[0];
670
- if (config.languageList.includes(lang2) && lang === lang2) {
671
- return true;
672
- }
673
- });
674
- if (curLangPugTem) {
675
- flag = true;
676
- pugTemplateArr = [curLangPugTem];
677
- } else {
678
- //没有特殊模版的语言排除其他语言的特殊模版
679
- pugTemplateArr = data._template.filter((item) => {
680
- let lang2 = item.split(pathSymbol)[0];
681
- if (config.languageList.includes(lang2)) {
682
- return false;
1127
+ /**
1128
+ * 找到标签名和修饰符的结束位置
1129
+ * @param {string} tagName - 标签名
1130
+ * @param {string} line - 行内容
1131
+ * @returns {number} 结束位置索引
1132
+ */
1133
+ function findTagEnd(tagName, line) {
1134
+ let index = tagName.length;
1135
+
1136
+ // 跳过类名和ID修饰符
1137
+ while (index < line.length) {
1138
+ const char = line[index];
1139
+ if (char === "." || char === "#") {
1140
+ index++;
1141
+ while (index < line.length && /[\w-]/.test(line[index])) {
1142
+ index++;
683
1143
  }
684
- return true;
685
- });
1144
+ } else {
1145
+ break;
1146
+ }
686
1147
  }
687
1148
 
688
- await async.each(pugTemplateArr, (pugTemplate, callback) => {
689
- let funName = pugTemplate.split(pathSymbol).join("_").slice(0, -4);
690
- if (flag) {
691
- pugTemplate = pugTemplate.split(pathSymbol).slice(1).join(pathSymbol);
1149
+ return index;
1150
+ }
1151
+
1152
+ /**
1153
+ * 找到匹配的右括号
1154
+ * @param {string} str - 字符串
1155
+ * @param {number} start - 开始位置
1156
+ * @returns {number} 匹配括号的位置,-1表示未找到
1157
+ */
1158
+ function findMatchingParen(str, start) {
1159
+ let depth = 0;
1160
+ let i = start;
1161
+ let inString = false;
1162
+ let stringChar = "";
1163
+
1164
+ while (i < str.length) {
1165
+ const char = str[i];
1166
+
1167
+ // 处理字符串内容,避免字符串中的括号影响匹配
1168
+ if (!inString && (char === '"' || char === "'" || char === "`")) {
1169
+ inString = true;
1170
+ stringChar = char;
1171
+ } else if (inString && char === stringChar && str[i - 1] !== "\\") {
1172
+ inString = false;
1173
+ stringChar = "";
1174
+ } else if (!inString) {
1175
+ if (char === "(") {
1176
+ depth++;
1177
+ } else if (char === ")") {
1178
+ depth--;
1179
+ if (depth === 0) {
1180
+ return i;
1181
+ }
1182
+ }
692
1183
  }
693
- let html = PagesPugToFn[funName]({
694
- data,
695
- _pagePath: pugTemplate,
696
- common: commonData,
697
- });
698
- if (data.page_name) {
699
- pugTemplate =
700
- pugTemplate.split(pathSymbol).slice(0, -1).join(pathSymbol) +
701
- pathSymbol +
702
- data.page_name;
1184
+ i++;
1185
+ }
1186
+
1187
+ return -1; // 没有找到匹配的括号
1188
+ }
1189
+
1190
+ /**
1191
+ * 递归处理目录中的所有 pug 文件
1192
+ * @param {string} dir - 目录路径
1193
+ * @param {string} baseDir - 基础目录路径(用于计算相对路径)
1194
+ */
1195
+ async function processPugFilesInDirectory(dir, baseDir) {
1196
+ try {
1197
+ const files = await fse.readdir(dir);
1198
+
1199
+ for (const file of files) {
1200
+ const fullPath = path.join(dir, file);
1201
+ const stats = await fse.stat(fullPath);
1202
+
1203
+ if (stats.isDirectory()) {
1204
+ // 递归处理子目录
1205
+ await processPugFilesInDirectory(fullPath, baseDir);
1206
+ } else if (file.endsWith(".pug")) {
1207
+ // 处理 pug 文件
1208
+ try {
1209
+ const relativePath = path
1210
+ .relative(baseDir, fullPath)
1211
+ .replace(/\\/g, "/");
1212
+ const pugContent = await fse.readFile(fullPath, "utf8");
1213
+
1214
+ console.log(`正在处理: ${relativePath}`);
1215
+
1216
+ const processedContent = addDebugAttributesToPug(
1217
+ pugContent,
1218
+ relativePath,
1219
+ file
1220
+ );
1221
+
1222
+ // 写回处理后的内容
1223
+ await fse.writeFile(fullPath, processedContent, "utf8");
1224
+
1225
+ console.log(`已处理: ${relativePath}`);
1226
+ } catch (error) {
1227
+ console.error(`处理文件 ${fullPath} 时出错:`, error);
1228
+ }
1229
+ }
703
1230
  }
704
- let htmlPath = paths.resolveRoot(
705
- distOutputPath,
706
- lang,
707
- pugTemplate.replace(/\..*$/, ".html")
708
- );
709
- fse.ensureFileSync(htmlPath);
710
- const writeStream = fse.createWriteStream(htmlPath);
711
- writeStream.write(html);
712
- writeStream.end(callback);
713
- });
714
- });
715
- });
716
- console.log("打包完成花费:", (Date.now() - starTime) / 1000, "s");
1231
+ } catch (error) {
1232
+ console.error(`处理目录 ${dir} 时出错:`, error);
1233
+ }
1234
+ }
1235
+
1236
+ // 开始处理调试目录中的所有 pug 文件
1237
+ console.log("开始为 pug 文件添加调试属性...");
1238
+ await processPugFilesInDirectory(targetDir, targetDir);
1239
+
1240
+ console.log("调试模板目录创建完成!");
1241
+ console.log(`调试模板位置: ${targetDir}`);
1242
+ } catch (error) {
1243
+ console.error("创建调试模板时出错:", error);
1244
+ throw error;
1245
+ }
717
1246
  }