@mdfriday/foundry 25.9.3 → 25.9.5

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.
Files changed (138) hide show
  1. package/README.md +10 -0
  2. package/dist/internal/application/ssg.d.ts.map +1 -1
  3. package/dist/internal/application/ssg.js +12 -0
  4. package/dist/internal/application/ssg.js.map +1 -1
  5. package/dist/internal/application/test-incremental-ssg.d.ts +9 -0
  6. package/dist/internal/application/test-incremental-ssg.d.ts.map +1 -0
  7. package/dist/internal/application/test-incremental-ssg.js +80 -0
  8. package/dist/internal/application/test-incremental-ssg.js.map +1 -0
  9. package/dist/internal/application/test-ssg.d.ts +8 -0
  10. package/dist/internal/application/test-ssg.d.ts.map +1 -0
  11. package/dist/internal/application/test-ssg.js +30 -0
  12. package/dist/internal/application/test-ssg.js.map +1 -0
  13. package/dist/internal/domain/config/entity/config.d.ts +8 -2
  14. package/dist/internal/domain/config/entity/config.d.ts.map +1 -1
  15. package/dist/internal/domain/config/entity/config.js +10 -3
  16. package/dist/internal/domain/config/entity/config.js.map +1 -1
  17. package/dist/internal/domain/config/entity/social.d.ts +66 -0
  18. package/dist/internal/domain/config/entity/social.d.ts.map +1 -0
  19. package/dist/internal/domain/config/entity/social.js +126 -0
  20. package/dist/internal/domain/config/entity/social.js.map +1 -0
  21. package/dist/internal/domain/config/factory/config.d.ts.map +1 -1
  22. package/dist/internal/domain/config/factory/config.js +2 -1
  23. package/dist/internal/domain/config/factory/config.js.map +1 -1
  24. package/dist/internal/domain/config/index.d.ts +2 -0
  25. package/dist/internal/domain/config/index.d.ts.map +1 -1
  26. package/dist/internal/domain/config/index.js +5 -1
  27. package/dist/internal/domain/config/index.js.map +1 -1
  28. package/dist/internal/domain/config/tests/entity-config.test.js +16 -12
  29. package/dist/internal/domain/config/tests/entity-config.test.js.map +1 -1
  30. package/dist/internal/domain/config/tests/social.test.d.ts +2 -0
  31. package/dist/internal/domain/config/tests/social.test.d.ts.map +1 -0
  32. package/dist/internal/domain/config/tests/social.test.js +164 -0
  33. package/dist/internal/domain/config/tests/social.test.js.map +1 -0
  34. package/dist/internal/domain/config/type.d.ts +22 -0
  35. package/dist/internal/domain/config/type.d.ts.map +1 -1
  36. package/dist/internal/domain/config/type.js.map +1 -1
  37. package/dist/internal/domain/config/vo/social.d.ts +23 -0
  38. package/dist/internal/domain/config/vo/social.d.ts.map +1 -0
  39. package/dist/internal/domain/config/vo/social.js +212 -0
  40. package/dist/internal/domain/config/vo/social.js.map +1 -0
  41. package/dist/internal/domain/content/entity/page.d.ts +6 -1
  42. package/dist/internal/domain/content/entity/page.d.ts.map +1 -1
  43. package/dist/internal/domain/content/entity/page.js +14 -2
  44. package/dist/internal/domain/content/entity/page.js.map +1 -1
  45. package/dist/internal/domain/content/entity/pagebuilder.d.ts +3 -2
  46. package/dist/internal/domain/content/entity/pagebuilder.d.ts.map +1 -1
  47. package/dist/internal/domain/content/entity/pagebuilder.js +16 -5
  48. package/dist/internal/domain/content/entity/pagebuilder.js.map +1 -1
  49. package/dist/internal/domain/content/entity/pagemeta.d.ts +10 -2
  50. package/dist/internal/domain/content/entity/pagemeta.d.ts.map +1 -1
  51. package/dist/internal/domain/content/entity/pagemeta.js +17 -1
  52. package/dist/internal/domain/content/entity/pagemeta.js.map +1 -1
  53. package/dist/internal/domain/content/entity/paginator.d.ts +6 -0
  54. package/dist/internal/domain/content/entity/paginator.d.ts.map +1 -1
  55. package/dist/internal/domain/content/entity/paginator.js +24 -2
  56. package/dist/internal/domain/content/entity/paginator.js.map +1 -1
  57. package/dist/internal/domain/content/factory/hub.d.ts.map +1 -1
  58. package/dist/internal/domain/content/factory/hub.js +2 -1
  59. package/dist/internal/domain/content/factory/hub.js.map +1 -1
  60. package/dist/internal/domain/content/type.d.ts +59 -1
  61. package/dist/internal/domain/content/type.d.ts.map +1 -1
  62. package/dist/internal/domain/content/type.js.map +1 -1
  63. package/dist/internal/domain/content/vo/frontmatter.d.ts +29 -1
  64. package/dist/internal/domain/content/vo/frontmatter.d.ts.map +1 -1
  65. package/dist/internal/domain/content/vo/frontmatter.js +242 -1
  66. package/dist/internal/domain/content/vo/frontmatter.js.map +1 -1
  67. package/dist/internal/domain/markdown/test/external-adapter.test.d.ts +2 -0
  68. package/dist/internal/domain/markdown/test/external-adapter.test.d.ts.map +1 -0
  69. package/dist/internal/domain/markdown/test/external-adapter.test.js +150 -0
  70. package/dist/internal/domain/markdown/test/external-adapter.test.js.map +1 -0
  71. package/dist/internal/domain/markdown/test/markdown.test.d.ts +2 -0
  72. package/dist/internal/domain/markdown/test/markdown.test.d.ts.map +1 -0
  73. package/dist/internal/domain/markdown/test/markdown.test.js +88 -0
  74. package/dist/internal/domain/markdown/test/markdown.test.js.map +1 -0
  75. package/dist/internal/domain/markdown/vo/externaladapter.d.ts +63 -0
  76. package/dist/internal/domain/markdown/vo/externaladapter.d.ts.map +1 -0
  77. package/dist/internal/domain/markdown/vo/externaladapter.js +194 -0
  78. package/dist/internal/domain/markdown/vo/externaladapter.js.map +1 -0
  79. package/dist/internal/domain/markdown/vo/markdownit.d.ts +33 -0
  80. package/dist/internal/domain/markdown/vo/markdownit.d.ts.map +1 -0
  81. package/dist/internal/domain/markdown/vo/markdownit.js +175 -0
  82. package/dist/internal/domain/markdown/vo/markdownit.js.map +1 -0
  83. package/dist/internal/domain/module/entity/module.d.ts.map +1 -1
  84. package/dist/internal/domain/module/entity/module.js +10 -6
  85. package/dist/internal/domain/module/entity/module.js.map +1 -1
  86. package/dist/internal/domain/module/vo/themes.test.js +12 -8
  87. package/dist/internal/domain/module/vo/themes.test.js.map +1 -1
  88. package/dist/internal/domain/site/entity/menu-builder.d.ts +35 -21
  89. package/dist/internal/domain/site/entity/menu-builder.d.ts.map +1 -1
  90. package/dist/internal/domain/site/entity/menu-builder.js +194 -107
  91. package/dist/internal/domain/site/entity/menu-builder.js.map +1 -1
  92. package/dist/internal/domain/site/entity/page.d.ts +19 -7
  93. package/dist/internal/domain/site/entity/page.d.ts.map +1 -1
  94. package/dist/internal/domain/site/entity/page.js +58 -3
  95. package/dist/internal/domain/site/entity/page.js.map +1 -1
  96. package/dist/internal/domain/site/entity/pager.d.ts +2 -0
  97. package/dist/internal/domain/site/entity/pager.d.ts.map +1 -1
  98. package/dist/internal/domain/site/entity/pager.js +9 -0
  99. package/dist/internal/domain/site/entity/pager.js.map +1 -1
  100. package/dist/internal/domain/site/entity/site.d.ts +14 -8
  101. package/dist/internal/domain/site/entity/site.d.ts.map +1 -1
  102. package/dist/internal/domain/site/entity/site.js +35 -8
  103. package/dist/internal/domain/site/entity/site.js.map +1 -1
  104. package/dist/internal/domain/site/factory/navigation-factory.d.ts +4 -2
  105. package/dist/internal/domain/site/factory/navigation-factory.d.ts.map +1 -1
  106. package/dist/internal/domain/site/factory/navigation-factory.js +5 -4
  107. package/dist/internal/domain/site/factory/navigation-factory.js.map +1 -1
  108. package/dist/internal/domain/site/factory/site.d.ts.map +1 -1
  109. package/dist/internal/domain/site/factory/site.js +7 -9
  110. package/dist/internal/domain/site/factory/site.js.map +1 -1
  111. package/dist/internal/domain/site/type.d.ts +11 -0
  112. package/dist/internal/domain/site/type.d.ts.map +1 -1
  113. package/dist/internal/domain/site/valueobject/author.d.ts +4 -4
  114. package/dist/internal/domain/site/valueobject/author.d.ts.map +1 -1
  115. package/dist/internal/domain/site/valueobject/author.js +7 -4
  116. package/dist/internal/domain/site/valueobject/author.js.map +1 -1
  117. package/dist/internal/domain/site/valueobject/menu.d.ts +27 -115
  118. package/dist/internal/domain/site/valueobject/menu.d.ts.map +1 -1
  119. package/dist/internal/domain/site/valueobject/menu.js +42 -193
  120. package/dist/internal/domain/site/valueobject/menu.js.map +1 -1
  121. package/dist/internal/domain/site/valueobject/organization.d.ts +63 -0
  122. package/dist/internal/domain/site/valueobject/organization.d.ts.map +1 -0
  123. package/dist/internal/domain/site/valueobject/organization.js +94 -0
  124. package/dist/internal/domain/site/valueobject/organization.js.map +1 -0
  125. package/dist/internal/domain/site/valueobject/organization.test.d.ts +2 -0
  126. package/dist/internal/domain/site/valueobject/organization.test.d.ts.map +1 -0
  127. package/dist/internal/domain/site/valueobject/organization.test.js +78 -0
  128. package/dist/internal/domain/site/valueobject/organization.test.js.map +1 -0
  129. package/dist/internal/domain/template/vo/registry.d.ts +18 -0
  130. package/dist/internal/domain/template/vo/registry.d.ts.map +1 -1
  131. package/dist/internal/domain/template/vo/registry.js +343 -0
  132. package/dist/internal/domain/template/vo/registry.js.map +1 -1
  133. package/dist/pkg/log/manager.js +1 -1
  134. package/dist/pkg/maps/scratch.d.ts +1 -0
  135. package/dist/pkg/maps/scratch.d.ts.map +1 -1
  136. package/dist/pkg/maps/scratch.js +4 -0
  137. package/dist/pkg/maps/scratch.js.map +1 -1
  138. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../../../internal/domain/template/vo/registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAC,mBAAmB,EAAE,eAAe,EAAC,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAsFlD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IACtD,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzF,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACtF;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,gBAAgB;IAE9D;;;OAGG;IACH,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAgCrD;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B;;;OAGG;IACH,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAgBxF;;;OAGG;IACH,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI;IAiBpF;;;OAGG;IACH,OAAO,CAAC,gCAAgC;IAOxC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAgL5B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,0BAA0B;IAWlC;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IAuDjC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAuN/B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAmO7B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAgM7B;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAkXnC;;;OAGG;IACH,OAAO,CAAC,OAAO;IAuCf;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,WAAW;IAkCnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAoBjB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmEtB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkC9B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IA+I7B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAiI5B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAgDhC;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAqH7B;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IA6DlC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAmCxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAiCpB;;;OAGG;IACH,OAAO,CAAC,SAAS;IA0CjB;;;OAGG;IACH,OAAO,CAAC,SAAS;IA4DjB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAuChB;;OAEG;IACH,OAAO,CAAC,YAAY;IAgCpB;;;OAGG;IACH,OAAO,CAAC,cAAc;CAwDvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,eAAe,CAKnD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,eAAe,CAKpF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAQ3E"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../../../internal/domain/template/vo/registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAC,mBAAmB,EAAE,eAAe,EAAC,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAsFlD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IACtD,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzF,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACtF;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,gBAAgB;IAE9D;;;OAGG;IACH,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAgCrD;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B;;;OAGG;IACH,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAgBxF;;;OAGG;IACH,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI;IAiBpF;;;OAGG;IACH,OAAO,CAAC,gCAAgC;IAOxC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAgL5B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,0BAA0B;IAWlC;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IAuDjC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAuX/B;;OAEG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAgCpB;;OAEG;IACH,OAAO,CAAC,YAAY;IAoGpB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAmO7B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAgM7B;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IA2ZnC;;;OAGG;IACH,OAAO,CAAC,OAAO;IAuCf;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,WAAW;IAkCnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAoBjB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmEtB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkC9B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IA+I7B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAiI5B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAgDhC;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAqH7B;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IA4ElC;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAuCjB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAmCxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAiCpB;;;OAGG;IACH,OAAO,CAAC,SAAS;IA0CjB;;;OAGG;IACH,OAAO,CAAC,SAAS;IA4DjB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAuChB;;OAEG;IACH,OAAO,CAAC,YAAY;IAgCpB;;;OAGG;IACH,OAAO,CAAC,cAAc;CAwDvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,eAAe,CAKnD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,eAAe,CAKpF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAQ3E"}
@@ -629,6 +629,265 @@ class DefaultTemplateRegistry {
629
629
  return str.charAt(0).toUpperCase() + str.slice(1);
630
630
  }
631
631
  }));
632
+ // Substr extracts parts of a string, beginning at the character at the specified
633
+ // position, and returns the specified number of characters.
634
+ // It normally takes two parameters: start and length.
635
+ // It can also take one parameter: start, i.e. length is omitted, in which case
636
+ // the substring starting from start until the end of the string will be returned.
637
+ // To extract characters from the end of the string, use a negative start number.
638
+ // If length is given and is negative, then that many characters will be omitted from
639
+ // the end of string.
640
+ // Equivalent to Go's Substr function in strings namespace
641
+ // Usage: {{ substr "hello world" 0 5 }} -> "hello"
642
+ // Usage: {{ substr "hello world" 6 }} -> "world"
643
+ // Usage: {{ substr "hello world" -5 }} -> "world"
644
+ // Usage: {{ substr "hello world" 0 -6 }} -> "hello"
645
+ funcMap.set('substr', (a, ...nums) => {
646
+ // Convert input to string
647
+ const s = String(a);
648
+ // Convert string to array of characters to handle Unicode properly
649
+ const chars = Array.from(s);
650
+ const rlen = chars.length;
651
+ let start;
652
+ let length;
653
+ // Parse arguments
654
+ switch (nums.length) {
655
+ case 0:
656
+ throw new Error('too few arguments');
657
+ case 1:
658
+ // Only start parameter provided
659
+ const startNum = Number(nums[0]);
660
+ if (isNaN(startNum) || !Number.isInteger(startNum)) {
661
+ throw new Error('start argument must be an integer');
662
+ }
663
+ start = startNum;
664
+ length = rlen;
665
+ break;
666
+ case 2:
667
+ // Both start and length parameters provided
668
+ const startNum2 = Number(nums[0]);
669
+ const lengthNum = Number(nums[1]);
670
+ if (isNaN(startNum2) || !Number.isInteger(startNum2)) {
671
+ throw new Error('start argument must be an integer');
672
+ }
673
+ if (isNaN(lengthNum) || !Number.isInteger(lengthNum)) {
674
+ throw new Error('length argument must be an integer');
675
+ }
676
+ start = startNum2;
677
+ length = lengthNum;
678
+ break;
679
+ default:
680
+ throw new Error('too many arguments');
681
+ }
682
+ // Handle empty string
683
+ if (rlen === 0) {
684
+ return '';
685
+ }
686
+ // Handle negative start (count from end)
687
+ if (start < 0) {
688
+ start += rlen;
689
+ }
690
+ // If start was originally negative beyond rlen, set to 0
691
+ if (start < 0) {
692
+ start = 0;
693
+ }
694
+ // If start is beyond the string length, return empty
695
+ if (start > rlen - 1) {
696
+ return '';
697
+ }
698
+ let end = rlen;
699
+ // Handle different length cases
700
+ if (length === 0) {
701
+ return '';
702
+ }
703
+ else if (length < 0) {
704
+ // Negative length: omit that many characters from end
705
+ end += length;
706
+ }
707
+ else if (length > 0) {
708
+ // Positive length: take that many characters from start
709
+ end = start + length;
710
+ }
711
+ // Validate bounds
712
+ if (start >= end) {
713
+ return '';
714
+ }
715
+ if (end < 0) {
716
+ return '';
717
+ }
718
+ // Clamp end to string length
719
+ if (end > rlen) {
720
+ end = rlen;
721
+ }
722
+ // Extract substring
723
+ return chars.slice(start, end).join('');
724
+ });
725
+ // Truncate truncates the string to the specified length
726
+ // Equivalent to Go's Truncate function in strings namespace
727
+ // Usage: {{ truncate 10 "hello world" }} -> "hello …"
728
+ // Usage: {{ truncate 10 "..." "hello world" }} -> "hello..."
729
+ // Supports HTML-aware truncation
730
+ funcMap.set('truncate', (lengthParam, ...options) => {
731
+ // Parse length parameter
732
+ const length = Number(lengthParam);
733
+ if (isNaN(length) || !Number.isInteger(length)) {
734
+ throw new Error('truncate length must be an integer');
735
+ }
736
+ let textParam;
737
+ let ellipsis = ' …';
738
+ // Parse options
739
+ switch (options.length) {
740
+ case 0:
741
+ throw new Error('truncate requires a length and a string');
742
+ case 1:
743
+ textParam = options[0];
744
+ ellipsis = ' …';
745
+ break;
746
+ case 2:
747
+ ellipsis = String(options[0]);
748
+ textParam = options[1];
749
+ break;
750
+ default:
751
+ throw new Error('too many arguments passed to truncate');
752
+ }
753
+ // Convert text to string
754
+ const text = String(textParam);
755
+ // Check if input is HTML (simple heuristic: contains HTML tags)
756
+ const isHTML = /<[^>]+>/.test(text);
757
+ // Convert to array of characters for proper Unicode handling
758
+ const chars = Array.from(text);
759
+ // If text is already shorter than or equal to length, return as-is
760
+ if (chars.length <= length) {
761
+ if (isHTML) {
762
+ return text; // Return as HTML
763
+ }
764
+ return this.escapeHTML(text);
765
+ }
766
+ if (isHTML) {
767
+ return this.truncateHTML(text, length, ellipsis);
768
+ }
769
+ else {
770
+ return this.truncateText(text, length, ellipsis);
771
+ }
772
+ });
773
+ }
774
+ /**
775
+ * HTML escape utility function
776
+ */
777
+ escapeHTML(text) {
778
+ return text
779
+ .replace(/&/g, '&amp;')
780
+ .replace(/</g, '&lt;')
781
+ .replace(/>/g, '&gt;')
782
+ .replace(/"/g, '&quot;')
783
+ .replace(/'/g, '&#39;');
784
+ }
785
+ /**
786
+ * Truncate plain text, breaking at word boundaries when possible
787
+ */
788
+ truncateText(text, length, ellipsis) {
789
+ const chars = Array.from(text);
790
+ if (chars.length <= length) {
791
+ return this.escapeHTML(text);
792
+ }
793
+ let lastWordIndex = 0;
794
+ let currentLen = 0;
795
+ // Find the best truncation point (word boundary)
796
+ for (let i = 0; i < chars.length && currentLen < length; i++) {
797
+ const char = chars[i];
798
+ currentLen++;
799
+ if (/\s/.test(char)) {
800
+ lastWordIndex = i;
801
+ }
802
+ // For CJK characters, each character can be a word boundary
803
+ if (/[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(char)) {
804
+ lastWordIndex = i;
805
+ }
806
+ }
807
+ // Determine truncation point
808
+ let endPos = currentLen >= length ? (lastWordIndex > 0 ? lastWordIndex : length) : chars.length;
809
+ const truncated = chars.slice(0, endPos).join('');
810
+ return this.escapeHTML(truncated) + ellipsis;
811
+ }
812
+ /**
813
+ * Truncate HTML text while preserving tag structure
814
+ */
815
+ truncateHTML(text, length, ellipsis) {
816
+ const tagRegex = /^<(\/)?([^ ]+?)(?:(\s*\/)| .*?)?>/;
817
+ const htmlSinglets = new Set(['br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input']);
818
+ const tags = [];
819
+ let lastWordIndex = 0;
820
+ let lastNonSpace = 0;
821
+ let currentLen = 0;
822
+ let endTextPos = 0;
823
+ let i = 0;
824
+ const chars = Array.from(text);
825
+ while (i < chars.length) {
826
+ const currentChar = chars[i];
827
+ // Check for HTML tag
828
+ if (currentChar === '<') {
829
+ const remainingText = chars.slice(i).join('');
830
+ const match = remainingText.match(tagRegex);
831
+ if (match) {
832
+ const fullMatch = match[0];
833
+ const isClosing = match[1] === '/';
834
+ const tagName = match[2].toLowerCase();
835
+ const isSelfClosing = match[3] === '/';
836
+ // Skip the entire tag
837
+ i += fullMatch.length;
838
+ lastWordIndex = lastNonSpace;
839
+ // Track non-singlet tags for proper closing
840
+ const isSinglet = htmlSinglets.has(tagName);
841
+ if (!isSinglet && !isSelfClosing) {
842
+ tags.push({
843
+ name: tagName,
844
+ pos: i - fullMatch.length,
845
+ openTag: !isClosing
846
+ });
847
+ }
848
+ continue;
849
+ }
850
+ }
851
+ // Count actual text characters
852
+ currentLen++;
853
+ if (/\s/.test(currentChar)) {
854
+ lastWordIndex = lastNonSpace;
855
+ }
856
+ else if (/[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(currentChar)) {
857
+ // CJK characters can break at any character
858
+ lastWordIndex = i;
859
+ }
860
+ else {
861
+ lastNonSpace = i + 1;
862
+ }
863
+ if (currentLen > length) {
864
+ endTextPos = lastWordIndex > 0 ? lastWordIndex : i;
865
+ break;
866
+ }
867
+ i++;
868
+ }
869
+ if (currentLen <= length) {
870
+ return text; // No truncation needed
871
+ }
872
+ let output = chars.slice(0, endTextPos).join('') + ellipsis;
873
+ // Close any open HTML tags
874
+ let currentTag = null;
875
+ for (let j = tags.length - 1; j >= 0; j--) {
876
+ const tag = tags[j];
877
+ if (tag.pos >= endTextPos || currentTag !== null) {
878
+ if (currentTag !== null && currentTag.name === tag.name) {
879
+ currentTag = null;
880
+ }
881
+ continue;
882
+ }
883
+ if (tag.openTag) {
884
+ output += `</${tag.name}>`;
885
+ }
886
+ else {
887
+ currentTag = tag;
888
+ }
889
+ }
890
+ return output;
632
891
  }
633
892
  /**
634
893
  * Register math functions
@@ -1355,6 +1614,42 @@ class DefaultTemplateRegistry {
1355
1614
  .join('&');
1356
1615
  return queryString;
1357
1616
  });
1617
+ // IsSet returns whether a given array, slice, or map has the given key defined
1618
+ // Equivalent to Go's IsSet function in collections namespace
1619
+ // Usage: {{ isset .array 0 }} or {{ isset .object "key" }}
1620
+ funcMap.set('isset', (c, key) => {
1621
+ if (c === null || c === undefined) {
1622
+ return false;
1623
+ }
1624
+ // Handle arrays and slices
1625
+ if (Array.isArray(c)) {
1626
+ // Convert key to integer for array indexing
1627
+ const k = Number(key);
1628
+ if (isNaN(k) || !Number.isInteger(k)) {
1629
+ console.warn(`isset unable to use key of type ${typeof key} as index`);
1630
+ return false;
1631
+ }
1632
+ // Check if index is valid (within bounds)
1633
+ return c.length > k && k >= 0;
1634
+ }
1635
+ // Handle objects/maps
1636
+ if (typeof c === 'object') {
1637
+ // For objects, check if the key exists as a property
1638
+ return key in c;
1639
+ }
1640
+ // Handle strings (similar to arrays, but for character indexing)
1641
+ if (typeof c === 'string') {
1642
+ const k = Number(key);
1643
+ if (isNaN(k) || !Number.isInteger(k)) {
1644
+ console.warn(`isset unable to use key of type ${typeof key} as index`);
1645
+ return false;
1646
+ }
1647
+ return c.length > k && k >= 0;
1648
+ }
1649
+ // Unsupported types
1650
+ console.warn(`calling isset with unsupported type "${typeof c}" (${c.constructor?.name || typeof c}) will always return false.`);
1651
+ return false;
1652
+ });
1358
1653
  }
1359
1654
  /**
1360
1655
  * Core indexing implementation following Go's doIndex logic
@@ -2023,6 +2318,14 @@ class DefaultTemplateRegistry {
2023
2318
  return null;
2024
2319
  }
2025
2320
  return this.unmarshalContent(dataStr, options);
2321
+ },
2322
+ // Plainify returns a copy of s with all HTML tags removed
2323
+ // Equivalent to Go's Plainify function in transform namespace
2324
+ // Usage: {{ "Hello <strong>world</strong>!" | transform.Plainify }} -> "Hello world!"
2325
+ // Usage: {{ transform.Plainify "<p>Text with <br> tags</p>" }} -> "Text with tags"
2326
+ Plainify: (s) => {
2327
+ const str = String(s);
2328
+ return this.stripHTML(str);
2026
2329
  }
2027
2330
  }));
2028
2331
  // Also register unmarshal as a standalone function for backward compatibility
@@ -2030,6 +2333,46 @@ class DefaultTemplateRegistry {
2030
2333
  const transform = funcMap.get('transform')();
2031
2334
  return transform.Unmarshal(...args);
2032
2335
  });
2336
+ // Register plainify as a standalone function for convenience
2337
+ funcMap.set('plainify', (s) => {
2338
+ const str = String(s);
2339
+ return this.stripHTML(str);
2340
+ });
2341
+ }
2342
+ /**
2343
+ * Strip HTML tags from a string
2344
+ * Equivalent to Go's helpers.StripHTML function
2345
+ * Preserves line breaks from block elements and normalizes whitespace
2346
+ */
2347
+ stripHTML(html) {
2348
+ // Shortcut for strings with no HTML tags
2349
+ if (!html.includes('<') && !html.includes('>')) {
2350
+ return html;
2351
+ }
2352
+ // First pass: Replace block elements that should create line breaks
2353
+ let result = html
2354
+ .replace(/\n/g, ' ') // Convert newlines to spaces first
2355
+ .replace(/<\/p>/gi, '\n') // </p> creates line break
2356
+ .replace(/<br\s*\/?>/gi, '\n') // <br> and <br/> create line breaks
2357
+ .replace(/<\/div>/gi, '\n') // </div> creates line break
2358
+ .replace(/<\/h[1-6]>/gi, '\n') // Heading end tags create line breaks
2359
+ .replace(/<\/li>/gi, '\n') // List items create line breaks
2360
+ .replace(/<\/tr>/gi, '\n') // Table rows create line breaks
2361
+ .replace(/<\/blockquote>/gi, '\n'); // Blockquotes create line breaks
2362
+ // Second pass: Remove all HTML tags
2363
+ result = result.replace(/<[^>]*>/g, '');
2364
+ // Third pass: Normalize whitespace
2365
+ // Convert multiple spaces to single spaces, but preserve intentional line breaks
2366
+ const lines = result.split('\n');
2367
+ const normalizedLines = lines.map(line => line.trim().replace(/\s+/g, ' '));
2368
+ // Join lines back together, removing empty lines but preserving intentional breaks
2369
+ result = normalizedLines
2370
+ .filter(line => line.length > 0)
2371
+ .join('\n')
2372
+ .trim();
2373
+ // Final cleanup: ensure we don't have multiple consecutive newlines
2374
+ result = result.replace(/\n\s*\n/g, '\n');
2375
+ return result;
2033
2376
  }
2034
2377
  /**
2035
2378
  * Unmarshal content from string based on format