@changerawr/markdown 1.1.4 → 1.1.6

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/index.js CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  ImageExtension: () => ImageExtension,
44
44
  InlineCodeExtension: () => InlineCodeExtension,
45
45
  ItalicExtension: () => ItalicExtension,
46
+ LRUCache: () => LRUCache,
46
47
  LineBreakExtension: () => LineBreakExtension,
47
48
  LinkExtension: () => LinkExtension,
48
49
  ListExtension: () => ListExtension,
@@ -75,10 +76,12 @@ __export(index_exports, {
75
76
  generateId: () => generateId,
76
77
  getASTStats: () => getASTStats,
77
78
  getTokenStats: () => getTokenStats,
79
+ hashContent: () => hashContent,
78
80
  isBrowser: () => isBrowser,
79
81
  isNode: () => isNode,
80
82
  isValidUrl: () => isValidUrl,
81
83
  markdown: () => markdown2,
84
+ memoize: () => memoize,
82
85
  minimalClasses: () => minimalClasses,
83
86
  parseASTFromJSON: () => parseASTFromJSON,
84
87
  parseCum: () => parseCum,
@@ -108,9 +111,11 @@ module.exports = __toCommonJS(index_exports);
108
111
 
109
112
  // src/parser.ts
110
113
  var MarkdownParser = class {
114
+ // Cache compiled regexes
111
115
  constructor(config) {
112
116
  this.rules = [];
113
117
  this.warnings = [];
118
+ this.compiledPatterns = /* @__PURE__ */ new Map();
114
119
  this.config = {
115
120
  debugMode: false,
116
121
  maxIterations: 1e4,
@@ -120,6 +125,10 @@ var MarkdownParser = class {
120
125
  }
121
126
  addRule(rule) {
122
127
  this.rules.push(rule);
128
+ this.compiledPatterns.set(
129
+ rule,
130
+ new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""))
131
+ );
123
132
  this.rules.sort((a, b) => {
124
133
  const aFeatureExtension = ["alert", "button", "embed"].includes(a.name);
125
134
  const bFeatureExtension = ["alert", "button", "embed"].includes(b.name);
@@ -165,12 +174,20 @@ var MarkdownParser = class {
165
174
  iterationCount++;
166
175
  let matched = false;
167
176
  let bestMatch = null;
177
+ let nextBestMatchIndex = null;
168
178
  for (const rule of this.rules) {
169
179
  try {
170
- const pattern = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
180
+ const pattern = this.compiledPatterns.get(rule);
171
181
  const match = remaining.match(pattern);
172
182
  if (match && match.index !== void 0) {
173
- const priority = match.index === 0 ? 1e3 : 1e3 - match.index;
183
+ if (match.index === 0) {
184
+ bestMatch = { rule, match, priority: 1e3 };
185
+ break;
186
+ }
187
+ if (nextBestMatchIndex === null || match.index < nextBestMatchIndex) {
188
+ nextBestMatchIndex = match.index;
189
+ }
190
+ const priority = 1e3 - match.index;
174
191
  if (!bestMatch || priority > bestMatch.priority || priority === bestMatch.priority && match.index < (bestMatch.match.index || 0)) {
175
192
  bestMatch = { rule, match, priority };
176
193
  }
@@ -217,15 +234,14 @@ var MarkdownParser = class {
217
234
  }
218
235
  }
219
236
  if (!matched) {
220
- const char = remaining[0];
221
- if (char) {
222
- tokens.push({
223
- type: "text",
224
- content: char,
225
- raw: char
226
- });
227
- remaining = remaining.slice(1);
228
- }
237
+ const chunkSize = nextBestMatchIndex !== null ? nextBestMatchIndex : Math.min(remaining.length, 1e3);
238
+ const textChunk = remaining.slice(0, chunkSize);
239
+ tokens.push({
240
+ type: "text",
241
+ content: textChunk,
242
+ raw: textChunk
243
+ });
244
+ remaining = remaining.slice(chunkSize);
229
245
  }
230
246
  }
231
247
  if (iterationCount >= maxIterations) {
@@ -305,7 +321,7 @@ var MarkdownParser = class {
305
321
  return processed;
306
322
  }
307
323
  recursivelyParseBlockContent(token) {
308
- const blockTypes = ["alert", "blockquote"];
324
+ const blockTypes = ["alert", "blockquote", "list-item", "task-item"];
309
325
  if (blockTypes.includes(token.type) && token.content && token.content.trim()) {
310
326
  const children = this.parse(token.content);
311
327
  return {
@@ -723,6 +739,171 @@ var MarkdownRenderer = class {
723
739
  }
724
740
  };
725
741
 
742
+ // src/cache.ts
743
+ var LRUCache = class {
744
+ constructor(capacity = 100) {
745
+ this.cache = /* @__PURE__ */ new Map();
746
+ this.hits = 0;
747
+ this.misses = 0;
748
+ this.evictions = 0;
749
+ if (capacity <= 0) {
750
+ throw new Error("Cache capacity must be greater than 0");
751
+ }
752
+ this.capacity = capacity;
753
+ }
754
+ /**
755
+ * Get a value from the cache
756
+ */
757
+ get(key) {
758
+ const entry = this.cache.get(key);
759
+ if (entry) {
760
+ entry.timestamp = Date.now();
761
+ entry.accessCount++;
762
+ this.hits++;
763
+ this.cache.delete(key);
764
+ this.cache.set(key, entry);
765
+ return entry.value;
766
+ }
767
+ this.misses++;
768
+ return void 0;
769
+ }
770
+ /**
771
+ * Set a value in the cache
772
+ */
773
+ set(key, value) {
774
+ if (this.cache.has(key)) {
775
+ this.cache.delete(key);
776
+ } else if (this.cache.size >= this.capacity) {
777
+ this.evictLRU();
778
+ }
779
+ this.cache.set(key, {
780
+ value,
781
+ timestamp: Date.now(),
782
+ accessCount: 0
783
+ });
784
+ }
785
+ /**
786
+ * Check if a key exists in the cache
787
+ */
788
+ has(key) {
789
+ return this.cache.has(key);
790
+ }
791
+ /**
792
+ * Delete a specific key from the cache
793
+ */
794
+ delete(key) {
795
+ return this.cache.delete(key);
796
+ }
797
+ /**
798
+ * Clear all entries from the cache
799
+ */
800
+ clear() {
801
+ this.cache.clear();
802
+ this.hits = 0;
803
+ this.misses = 0;
804
+ this.evictions = 0;
805
+ }
806
+ /**
807
+ * Get the current size of the cache
808
+ */
809
+ get size() {
810
+ return this.cache.size;
811
+ }
812
+ /**
813
+ * Get cache statistics
814
+ */
815
+ getStats() {
816
+ const totalRequests = this.hits + this.misses;
817
+ return {
818
+ size: this.cache.size,
819
+ capacity: this.capacity,
820
+ hits: this.hits,
821
+ misses: this.misses,
822
+ hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
823
+ evictions: this.evictions
824
+ };
825
+ }
826
+ /**
827
+ * Reset cache statistics
828
+ */
829
+ resetStats() {
830
+ this.hits = 0;
831
+ this.misses = 0;
832
+ this.evictions = 0;
833
+ }
834
+ /**
835
+ * Get all keys in the cache
836
+ */
837
+ keys() {
838
+ return Array.from(this.cache.keys());
839
+ }
840
+ /**
841
+ * Get all values in the cache
842
+ */
843
+ values() {
844
+ return Array.from(this.cache.values()).map((entry) => entry.value);
845
+ }
846
+ /**
847
+ * Update cache capacity and evict if necessary
848
+ */
849
+ setCapacity(newCapacity) {
850
+ if (newCapacity <= 0) {
851
+ throw new Error("Cache capacity must be greater than 0");
852
+ }
853
+ this.capacity = newCapacity;
854
+ while (this.cache.size > this.capacity) {
855
+ this.evictLRU();
856
+ }
857
+ }
858
+ /**
859
+ * Evict the least recently used entry
860
+ */
861
+ evictLRU() {
862
+ const firstKey = this.cache.keys().next().value;
863
+ if (firstKey !== void 0) {
864
+ this.cache.delete(firstKey);
865
+ this.evictions++;
866
+ }
867
+ }
868
+ };
869
+ function hashContent(content) {
870
+ if (content.length > 1e4) {
871
+ const start = content.slice(0, 1e3);
872
+ const middle = content.slice(Math.floor(content.length / 2) - 500, Math.floor(content.length / 2) + 500);
873
+ const end = content.slice(-1e3);
874
+ const sample = content.length + "|" + start + middle + end;
875
+ let hash2 = 2166136261;
876
+ for (let i = 0; i < sample.length; i++) {
877
+ hash2 ^= sample.charCodeAt(i);
878
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
879
+ }
880
+ return (hash2 >>> 0).toString(36);
881
+ }
882
+ let hash = 2166136261;
883
+ for (let i = 0; i < content.length; i++) {
884
+ hash ^= content.charCodeAt(i);
885
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
886
+ }
887
+ return (hash >>> 0).toString(36);
888
+ }
889
+ function memoize(fn, options = {}) {
890
+ const cache = options.cache || new LRUCache(options.maxSize || 100);
891
+ const keyGenerator = options.keyGenerator || ((...args) => JSON.stringify(args));
892
+ const memoized = function(...args) {
893
+ const key = keyGenerator(...args);
894
+ const cached = cache.get(key);
895
+ if (cached !== void 0) {
896
+ return cached;
897
+ }
898
+ const result = fn.apply(this, args);
899
+ cache.set(key, result);
900
+ return result;
901
+ };
902
+ memoized.cache = cache;
903
+ memoized.clearCache = () => cache.clear();
904
+ return memoized;
905
+ }
906
+
726
907
  // src/extensions/core/blockquote.ts
727
908
  var BlockquoteExtension = {
728
909
  name: "blockquote",
@@ -1096,7 +1277,14 @@ var ListExtension = {
1096
1277
  renderRules: [
1097
1278
  {
1098
1279
  type: "list-item",
1099
- render: (token) => `<li>${escapeHtml(token.content)}</li>`
1280
+ render: (token) => {
1281
+ const format = token.attributes?.format || "tailwind";
1282
+ const content = token.attributes?.renderedChildren || escapeHtml(token.content);
1283
+ if (format === "html") {
1284
+ return `<li>${content}</li>`;
1285
+ }
1286
+ return `<li>${content}</li>`;
1287
+ }
1100
1288
  }
1101
1289
  ]
1102
1290
  };
@@ -1121,18 +1309,18 @@ var TaskListExtension = {
1121
1309
  type: "task-item",
1122
1310
  render: (token) => {
1123
1311
  const isChecked = token.attributes?.checked === "true";
1124
- const escapedContent = escapeHtml(token.content);
1312
+ const content = token.attributes?.renderedChildren || escapeHtml(token.content);
1125
1313
  const format = token.attributes?.format || "html";
1126
1314
  if (format === "html") {
1127
1315
  return `<div style="display: flex; align-items: center; gap: 8px; margin: 8px 0;">
1128
1316
  <input type="checkbox" ${isChecked ? "checked" : ""} disabled style="margin: 0;" />
1129
- <span${isChecked ? ' style="text-decoration: line-through; color: #6b7280;"' : ""}>${escapedContent}</span>
1317
+ <span${isChecked ? ' style="text-decoration: line-through; color: #6b7280;"' : ""}>${content}</span>
1130
1318
  </div>`;
1131
1319
  }
1132
1320
  return `<div class="flex items-center gap-2 my-2 task-list-item">
1133
- <input type="checkbox" ${isChecked ? "checked" : ""} disabled
1321
+ <input type="checkbox" ${isChecked ? "checked" : ""} disabled
1134
1322
  class="form-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" />
1135
- <span${isChecked ? ' class="line-through text-muted-foreground"' : ""}>${escapedContent}</span>
1323
+ <span${isChecked ? ' class="line-through text-muted-foreground"' : ""}>${content}</span>
1136
1324
  </div>`;
1137
1325
  }
1138
1326
  }
@@ -1200,7 +1388,7 @@ var AlertExtension = {
1200
1388
  parseRules: [
1201
1389
  {
1202
1390
  name: "alert",
1203
- pattern: /:::(\w+)(?:\s+(.*?))?\s*\n([\s\S]*?)\n:::/,
1391
+ pattern: /:::(\w+)(?: ([^\n]+))?\n([\s\S]*?)\n:::/,
1204
1392
  render: (match) => {
1205
1393
  return {
1206
1394
  type: "alert",
@@ -1208,7 +1396,7 @@ var AlertExtension = {
1208
1396
  raw: match[0] || "",
1209
1397
  attributes: {
1210
1398
  type: match[1] || "info",
1211
- title: match[2] || ""
1399
+ title: match[2]?.trim() || ""
1212
1400
  }
1213
1401
  };
1214
1402
  }
@@ -1688,8 +1876,13 @@ function extractVimeoId(url) {
1688
1876
  var ChangerawrMarkdown = class {
1689
1877
  constructor(config) {
1690
1878
  this.extensions = /* @__PURE__ */ new Map();
1879
+ this.parseTime = 0;
1880
+ this.renderTime = 0;
1881
+ this.lastTokenCount = 0;
1691
1882
  this.parser = new MarkdownParser(config?.parser);
1692
1883
  this.renderer = new MarkdownRenderer(config?.renderer);
1884
+ this.parseCache = new LRUCache(100);
1885
+ this.renderCache = new LRUCache(100);
1693
1886
  this.registerCoreExtensions();
1694
1887
  this.registerFeatureExtensions();
1695
1888
  if (config?.extensions) {
@@ -1772,14 +1965,83 @@ var ChangerawrMarkdown = class {
1772
1965
  }
1773
1966
  }
1774
1967
  parse(markdown3) {
1775
- return this.parser.parse(markdown3);
1776
- }
1777
- render(tokens) {
1778
- return this.renderer.render(tokens);
1968
+ const cacheKey = hashContent(markdown3);
1969
+ const cached = this.parseCache.get(cacheKey);
1970
+ if (cached) {
1971
+ this.lastTokenCount = cached.length;
1972
+ return cached;
1973
+ }
1974
+ const startTime = performance.now();
1975
+ const tokens = this.parser.parse(markdown3);
1976
+ this.parseTime = performance.now() - startTime;
1977
+ this.lastTokenCount = tokens.length;
1978
+ this.parseCache.set(cacheKey, tokens);
1979
+ return tokens;
1980
+ }
1981
+ render(tokens, cacheKey) {
1982
+ if (cacheKey) {
1983
+ const cached = this.renderCache.get(cacheKey);
1984
+ if (cached) {
1985
+ return cached;
1986
+ }
1987
+ }
1988
+ const startTime = performance.now();
1989
+ const html = this.renderer.render(tokens);
1990
+ this.renderTime = performance.now() - startTime;
1991
+ if (cacheKey) {
1992
+ this.renderCache.set(cacheKey, html);
1993
+ }
1994
+ return html;
1779
1995
  }
1780
1996
  toHtml(markdown3) {
1997
+ const cacheKey = hashContent(markdown3);
1998
+ const cachedHtml = this.renderCache.get(cacheKey);
1999
+ if (cachedHtml) {
2000
+ return cachedHtml;
2001
+ }
1781
2002
  const tokens = this.parse(markdown3);
1782
- return this.render(tokens);
2003
+ return this.render(tokens, cacheKey);
2004
+ }
2005
+ /**
2006
+ * Render markdown with performance metrics
2007
+ */
2008
+ toHtmlWithMetrics(markdown3) {
2009
+ const startTotal = performance.now();
2010
+ const parseCacheKey = hashContent(markdown3);
2011
+ const parseCacheHit = this.parseCache.has(parseCacheKey);
2012
+ const html = this.toHtml(markdown3);
2013
+ const totalTime = performance.now() - startTotal;
2014
+ const metrics = {
2015
+ inputSize: markdown3.length,
2016
+ parseTime: this.parseTime,
2017
+ renderTime: this.renderTime,
2018
+ totalTime,
2019
+ tokenCount: this.lastTokenCount,
2020
+ cacheHit: parseCacheHit
2021
+ };
2022
+ return { html, metrics };
2023
+ }
2024
+ /**
2025
+ * Stream-render large documents in chunks for better performance
2026
+ */
2027
+ async toHtmlStreamed(markdown3, options = {}) {
2028
+ const chunkSize = options.chunkSize || 50;
2029
+ const tokens = this.parse(markdown3);
2030
+ const totalTokens = tokens.length;
2031
+ const chunks = [];
2032
+ for (let i = 0; i < tokens.length; i += chunkSize) {
2033
+ const chunkTokens = tokens.slice(i, Math.min(i + chunkSize, tokens.length));
2034
+ const chunkHtml = this.render(chunkTokens);
2035
+ chunks.push(chunkHtml);
2036
+ if (options.onChunk) {
2037
+ options.onChunk({
2038
+ html: chunkHtml,
2039
+ progress: Math.min(i + chunkSize, tokens.length) / totalTokens
2040
+ });
2041
+ }
2042
+ await new Promise((resolve) => setTimeout(resolve, 0));
2043
+ }
2044
+ return chunks.join("");
1783
2045
  }
1784
2046
  getExtensions() {
1785
2047
  return Array.from(this.extensions.keys());
@@ -1793,25 +2055,50 @@ var ChangerawrMarkdown = class {
1793
2055
  getDebugInfo() {
1794
2056
  return {
1795
2057
  warnings: this.getWarnings(),
1796
- parseTime: 0,
1797
- renderTime: 0,
1798
- tokenCount: 0,
2058
+ parseTime: this.parseTime,
2059
+ renderTime: this.renderTime,
2060
+ tokenCount: this.lastTokenCount,
1799
2061
  iterationCount: 0
1800
2062
  };
1801
2063
  }
1802
2064
  getPerformanceMetrics() {
1803
2065
  return {
1804
- parseTime: 0,
1805
- renderTime: 0,
1806
- totalTime: 0,
1807
- tokenCount: 0
2066
+ parseTime: this.parseTime,
2067
+ renderTime: this.renderTime,
2068
+ totalTime: this.parseTime + this.renderTime,
2069
+ tokenCount: this.lastTokenCount
1808
2070
  };
1809
2071
  }
2072
+ /**
2073
+ * Get cache statistics
2074
+ */
2075
+ getCacheStats() {
2076
+ return {
2077
+ parse: this.parseCache.getStats(),
2078
+ render: this.renderCache.getStats()
2079
+ };
2080
+ }
2081
+ /**
2082
+ * Clear all caches
2083
+ */
2084
+ clearCaches() {
2085
+ this.parseCache.clear();
2086
+ this.renderCache.clear();
2087
+ }
2088
+ /**
2089
+ * Update cache capacity
2090
+ */
2091
+ setCacheSize(size) {
2092
+ this.parseCache.setCapacity(size);
2093
+ this.renderCache.setCapacity(size);
2094
+ }
1810
2095
  rebuildParserAndRenderer() {
1811
2096
  const parserConfig = this.parser.getConfig();
1812
2097
  const rendererConfig = this.renderer.getConfig();
1813
2098
  this.parser = new MarkdownParser(parserConfig);
1814
2099
  this.renderer = new MarkdownRenderer(rendererConfig);
2100
+ this.parseCache.clear();
2101
+ this.renderCache.clear();
1815
2102
  const extensionsToRegister = Array.from(this.extensions.values());
1816
2103
  const featureExtensions = extensionsToRegister.filter(
1817
2104
  (ext) => ["alert", "button", "embed"].includes(ext.name)
@@ -2457,6 +2744,7 @@ function createEngineWithPreset(presetName, additionalConfig) {
2457
2744
  ImageExtension,
2458
2745
  InlineCodeExtension,
2459
2746
  ItalicExtension,
2747
+ LRUCache,
2460
2748
  LineBreakExtension,
2461
2749
  LinkExtension,
2462
2750
  ListExtension,
@@ -2488,10 +2776,12 @@ function createEngineWithPreset(presetName, additionalConfig) {
2488
2776
  generateId,
2489
2777
  getASTStats,
2490
2778
  getTokenStats,
2779
+ hashContent,
2491
2780
  isBrowser,
2492
2781
  isNode,
2493
2782
  isValidUrl,
2494
2783
  markdown,
2784
+ memoize,
2495
2785
  minimalClasses,
2496
2786
  parseASTFromJSON,
2497
2787
  parseCum,