@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.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  // src/parser.ts
2
2
  var MarkdownParser = class {
3
+ // Cache compiled regexes
3
4
  constructor(config) {
4
5
  this.rules = [];
5
6
  this.warnings = [];
7
+ this.compiledPatterns = /* @__PURE__ */ new Map();
6
8
  this.config = {
7
9
  debugMode: false,
8
10
  maxIterations: 1e4,
@@ -12,6 +14,10 @@ var MarkdownParser = class {
12
14
  }
13
15
  addRule(rule) {
14
16
  this.rules.push(rule);
17
+ this.compiledPatterns.set(
18
+ rule,
19
+ new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""))
20
+ );
15
21
  this.rules.sort((a, b) => {
16
22
  const aFeatureExtension = ["alert", "button", "embed"].includes(a.name);
17
23
  const bFeatureExtension = ["alert", "button", "embed"].includes(b.name);
@@ -57,12 +63,20 @@ var MarkdownParser = class {
57
63
  iterationCount++;
58
64
  let matched = false;
59
65
  let bestMatch = null;
66
+ let nextBestMatchIndex = null;
60
67
  for (const rule of this.rules) {
61
68
  try {
62
- const pattern = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
69
+ const pattern = this.compiledPatterns.get(rule);
63
70
  const match = remaining.match(pattern);
64
71
  if (match && match.index !== void 0) {
65
- const priority = match.index === 0 ? 1e3 : 1e3 - match.index;
72
+ if (match.index === 0) {
73
+ bestMatch = { rule, match, priority: 1e3 };
74
+ break;
75
+ }
76
+ if (nextBestMatchIndex === null || match.index < nextBestMatchIndex) {
77
+ nextBestMatchIndex = match.index;
78
+ }
79
+ const priority = 1e3 - match.index;
66
80
  if (!bestMatch || priority > bestMatch.priority || priority === bestMatch.priority && match.index < (bestMatch.match.index || 0)) {
67
81
  bestMatch = { rule, match, priority };
68
82
  }
@@ -109,15 +123,14 @@ var MarkdownParser = class {
109
123
  }
110
124
  }
111
125
  if (!matched) {
112
- const char = remaining[0];
113
- if (char) {
114
- tokens.push({
115
- type: "text",
116
- content: char,
117
- raw: char
118
- });
119
- remaining = remaining.slice(1);
120
- }
126
+ const chunkSize = nextBestMatchIndex !== null ? nextBestMatchIndex : Math.min(remaining.length, 1e3);
127
+ const textChunk = remaining.slice(0, chunkSize);
128
+ tokens.push({
129
+ type: "text",
130
+ content: textChunk,
131
+ raw: textChunk
132
+ });
133
+ remaining = remaining.slice(chunkSize);
121
134
  }
122
135
  }
123
136
  if (iterationCount >= maxIterations) {
@@ -197,7 +210,7 @@ var MarkdownParser = class {
197
210
  return processed;
198
211
  }
199
212
  recursivelyParseBlockContent(token) {
200
- const blockTypes = ["alert", "blockquote"];
213
+ const blockTypes = ["alert", "blockquote", "list-item", "task-item"];
201
214
  if (blockTypes.includes(token.type) && token.content && token.content.trim()) {
202
215
  const children = this.parse(token.content);
203
216
  return {
@@ -615,6 +628,171 @@ var MarkdownRenderer = class {
615
628
  }
616
629
  };
617
630
 
631
+ // src/cache.ts
632
+ var LRUCache = class {
633
+ constructor(capacity = 100) {
634
+ this.cache = /* @__PURE__ */ new Map();
635
+ this.hits = 0;
636
+ this.misses = 0;
637
+ this.evictions = 0;
638
+ if (capacity <= 0) {
639
+ throw new Error("Cache capacity must be greater than 0");
640
+ }
641
+ this.capacity = capacity;
642
+ }
643
+ /**
644
+ * Get a value from the cache
645
+ */
646
+ get(key) {
647
+ const entry = this.cache.get(key);
648
+ if (entry) {
649
+ entry.timestamp = Date.now();
650
+ entry.accessCount++;
651
+ this.hits++;
652
+ this.cache.delete(key);
653
+ this.cache.set(key, entry);
654
+ return entry.value;
655
+ }
656
+ this.misses++;
657
+ return void 0;
658
+ }
659
+ /**
660
+ * Set a value in the cache
661
+ */
662
+ set(key, value) {
663
+ if (this.cache.has(key)) {
664
+ this.cache.delete(key);
665
+ } else if (this.cache.size >= this.capacity) {
666
+ this.evictLRU();
667
+ }
668
+ this.cache.set(key, {
669
+ value,
670
+ timestamp: Date.now(),
671
+ accessCount: 0
672
+ });
673
+ }
674
+ /**
675
+ * Check if a key exists in the cache
676
+ */
677
+ has(key) {
678
+ return this.cache.has(key);
679
+ }
680
+ /**
681
+ * Delete a specific key from the cache
682
+ */
683
+ delete(key) {
684
+ return this.cache.delete(key);
685
+ }
686
+ /**
687
+ * Clear all entries from the cache
688
+ */
689
+ clear() {
690
+ this.cache.clear();
691
+ this.hits = 0;
692
+ this.misses = 0;
693
+ this.evictions = 0;
694
+ }
695
+ /**
696
+ * Get the current size of the cache
697
+ */
698
+ get size() {
699
+ return this.cache.size;
700
+ }
701
+ /**
702
+ * Get cache statistics
703
+ */
704
+ getStats() {
705
+ const totalRequests = this.hits + this.misses;
706
+ return {
707
+ size: this.cache.size,
708
+ capacity: this.capacity,
709
+ hits: this.hits,
710
+ misses: this.misses,
711
+ hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
712
+ evictions: this.evictions
713
+ };
714
+ }
715
+ /**
716
+ * Reset cache statistics
717
+ */
718
+ resetStats() {
719
+ this.hits = 0;
720
+ this.misses = 0;
721
+ this.evictions = 0;
722
+ }
723
+ /**
724
+ * Get all keys in the cache
725
+ */
726
+ keys() {
727
+ return Array.from(this.cache.keys());
728
+ }
729
+ /**
730
+ * Get all values in the cache
731
+ */
732
+ values() {
733
+ return Array.from(this.cache.values()).map((entry) => entry.value);
734
+ }
735
+ /**
736
+ * Update cache capacity and evict if necessary
737
+ */
738
+ setCapacity(newCapacity) {
739
+ if (newCapacity <= 0) {
740
+ throw new Error("Cache capacity must be greater than 0");
741
+ }
742
+ this.capacity = newCapacity;
743
+ while (this.cache.size > this.capacity) {
744
+ this.evictLRU();
745
+ }
746
+ }
747
+ /**
748
+ * Evict the least recently used entry
749
+ */
750
+ evictLRU() {
751
+ const firstKey = this.cache.keys().next().value;
752
+ if (firstKey !== void 0) {
753
+ this.cache.delete(firstKey);
754
+ this.evictions++;
755
+ }
756
+ }
757
+ };
758
+ function hashContent(content) {
759
+ if (content.length > 1e4) {
760
+ const start = content.slice(0, 1e3);
761
+ const middle = content.slice(Math.floor(content.length / 2) - 500, Math.floor(content.length / 2) + 500);
762
+ const end = content.slice(-1e3);
763
+ const sample = content.length + "|" + start + middle + end;
764
+ let hash2 = 2166136261;
765
+ for (let i = 0; i < sample.length; i++) {
766
+ hash2 ^= sample.charCodeAt(i);
767
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
768
+ }
769
+ return (hash2 >>> 0).toString(36);
770
+ }
771
+ let hash = 2166136261;
772
+ for (let i = 0; i < content.length; i++) {
773
+ hash ^= content.charCodeAt(i);
774
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
775
+ }
776
+ return (hash >>> 0).toString(36);
777
+ }
778
+ function memoize(fn, options = {}) {
779
+ const cache = options.cache || new LRUCache(options.maxSize || 100);
780
+ const keyGenerator = options.keyGenerator || ((...args) => JSON.stringify(args));
781
+ const memoized = function(...args) {
782
+ const key = keyGenerator(...args);
783
+ const cached = cache.get(key);
784
+ if (cached !== void 0) {
785
+ return cached;
786
+ }
787
+ const result = fn.apply(this, args);
788
+ cache.set(key, result);
789
+ return result;
790
+ };
791
+ memoized.cache = cache;
792
+ memoized.clearCache = () => cache.clear();
793
+ return memoized;
794
+ }
795
+
618
796
  // src/extensions/core/blockquote.ts
619
797
  var BlockquoteExtension = {
620
798
  name: "blockquote",
@@ -988,7 +1166,14 @@ var ListExtension = {
988
1166
  renderRules: [
989
1167
  {
990
1168
  type: "list-item",
991
- render: (token) => `<li>${escapeHtml(token.content)}</li>`
1169
+ render: (token) => {
1170
+ const format = token.attributes?.format || "tailwind";
1171
+ const content = token.attributes?.renderedChildren || escapeHtml(token.content);
1172
+ if (format === "html") {
1173
+ return `<li>${content}</li>`;
1174
+ }
1175
+ return `<li>${content}</li>`;
1176
+ }
992
1177
  }
993
1178
  ]
994
1179
  };
@@ -1013,18 +1198,18 @@ var TaskListExtension = {
1013
1198
  type: "task-item",
1014
1199
  render: (token) => {
1015
1200
  const isChecked = token.attributes?.checked === "true";
1016
- const escapedContent = escapeHtml(token.content);
1201
+ const content = token.attributes?.renderedChildren || escapeHtml(token.content);
1017
1202
  const format = token.attributes?.format || "html";
1018
1203
  if (format === "html") {
1019
1204
  return `<div style="display: flex; align-items: center; gap: 8px; margin: 8px 0;">
1020
1205
  <input type="checkbox" ${isChecked ? "checked" : ""} disabled style="margin: 0;" />
1021
- <span${isChecked ? ' style="text-decoration: line-through; color: #6b7280;"' : ""}>${escapedContent}</span>
1206
+ <span${isChecked ? ' style="text-decoration: line-through; color: #6b7280;"' : ""}>${content}</span>
1022
1207
  </div>`;
1023
1208
  }
1024
1209
  return `<div class="flex items-center gap-2 my-2 task-list-item">
1025
- <input type="checkbox" ${isChecked ? "checked" : ""} disabled
1210
+ <input type="checkbox" ${isChecked ? "checked" : ""} disabled
1026
1211
  class="form-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" />
1027
- <span${isChecked ? ' class="line-through text-muted-foreground"' : ""}>${escapedContent}</span>
1212
+ <span${isChecked ? ' class="line-through text-muted-foreground"' : ""}>${content}</span>
1028
1213
  </div>`;
1029
1214
  }
1030
1215
  }
@@ -1092,7 +1277,7 @@ var AlertExtension = {
1092
1277
  parseRules: [
1093
1278
  {
1094
1279
  name: "alert",
1095
- pattern: /:::(\w+)(?:\s+(.*?))?\s*\n([\s\S]*?)\n:::/,
1280
+ pattern: /:::(\w+)(?: ([^\n]+))?\n([\s\S]*?)\n:::/,
1096
1281
  render: (match) => {
1097
1282
  return {
1098
1283
  type: "alert",
@@ -1100,7 +1285,7 @@ var AlertExtension = {
1100
1285
  raw: match[0] || "",
1101
1286
  attributes: {
1102
1287
  type: match[1] || "info",
1103
- title: match[2] || ""
1288
+ title: match[2]?.trim() || ""
1104
1289
  }
1105
1290
  };
1106
1291
  }
@@ -1580,8 +1765,13 @@ function extractVimeoId(url) {
1580
1765
  var ChangerawrMarkdown = class {
1581
1766
  constructor(config) {
1582
1767
  this.extensions = /* @__PURE__ */ new Map();
1768
+ this.parseTime = 0;
1769
+ this.renderTime = 0;
1770
+ this.lastTokenCount = 0;
1583
1771
  this.parser = new MarkdownParser(config?.parser);
1584
1772
  this.renderer = new MarkdownRenderer(config?.renderer);
1773
+ this.parseCache = new LRUCache(100);
1774
+ this.renderCache = new LRUCache(100);
1585
1775
  this.registerCoreExtensions();
1586
1776
  this.registerFeatureExtensions();
1587
1777
  if (config?.extensions) {
@@ -1664,14 +1854,83 @@ var ChangerawrMarkdown = class {
1664
1854
  }
1665
1855
  }
1666
1856
  parse(markdown3) {
1667
- return this.parser.parse(markdown3);
1668
- }
1669
- render(tokens) {
1670
- return this.renderer.render(tokens);
1857
+ const cacheKey = hashContent(markdown3);
1858
+ const cached = this.parseCache.get(cacheKey);
1859
+ if (cached) {
1860
+ this.lastTokenCount = cached.length;
1861
+ return cached;
1862
+ }
1863
+ const startTime = performance.now();
1864
+ const tokens = this.parser.parse(markdown3);
1865
+ this.parseTime = performance.now() - startTime;
1866
+ this.lastTokenCount = tokens.length;
1867
+ this.parseCache.set(cacheKey, tokens);
1868
+ return tokens;
1869
+ }
1870
+ render(tokens, cacheKey) {
1871
+ if (cacheKey) {
1872
+ const cached = this.renderCache.get(cacheKey);
1873
+ if (cached) {
1874
+ return cached;
1875
+ }
1876
+ }
1877
+ const startTime = performance.now();
1878
+ const html = this.renderer.render(tokens);
1879
+ this.renderTime = performance.now() - startTime;
1880
+ if (cacheKey) {
1881
+ this.renderCache.set(cacheKey, html);
1882
+ }
1883
+ return html;
1671
1884
  }
1672
1885
  toHtml(markdown3) {
1886
+ const cacheKey = hashContent(markdown3);
1887
+ const cachedHtml = this.renderCache.get(cacheKey);
1888
+ if (cachedHtml) {
1889
+ return cachedHtml;
1890
+ }
1673
1891
  const tokens = this.parse(markdown3);
1674
- return this.render(tokens);
1892
+ return this.render(tokens, cacheKey);
1893
+ }
1894
+ /**
1895
+ * Render markdown with performance metrics
1896
+ */
1897
+ toHtmlWithMetrics(markdown3) {
1898
+ const startTotal = performance.now();
1899
+ const parseCacheKey = hashContent(markdown3);
1900
+ const parseCacheHit = this.parseCache.has(parseCacheKey);
1901
+ const html = this.toHtml(markdown3);
1902
+ const totalTime = performance.now() - startTotal;
1903
+ const metrics = {
1904
+ inputSize: markdown3.length,
1905
+ parseTime: this.parseTime,
1906
+ renderTime: this.renderTime,
1907
+ totalTime,
1908
+ tokenCount: this.lastTokenCount,
1909
+ cacheHit: parseCacheHit
1910
+ };
1911
+ return { html, metrics };
1912
+ }
1913
+ /**
1914
+ * Stream-render large documents in chunks for better performance
1915
+ */
1916
+ async toHtmlStreamed(markdown3, options = {}) {
1917
+ const chunkSize = options.chunkSize || 50;
1918
+ const tokens = this.parse(markdown3);
1919
+ const totalTokens = tokens.length;
1920
+ const chunks = [];
1921
+ for (let i = 0; i < tokens.length; i += chunkSize) {
1922
+ const chunkTokens = tokens.slice(i, Math.min(i + chunkSize, tokens.length));
1923
+ const chunkHtml = this.render(chunkTokens);
1924
+ chunks.push(chunkHtml);
1925
+ if (options.onChunk) {
1926
+ options.onChunk({
1927
+ html: chunkHtml,
1928
+ progress: Math.min(i + chunkSize, tokens.length) / totalTokens
1929
+ });
1930
+ }
1931
+ await new Promise((resolve) => setTimeout(resolve, 0));
1932
+ }
1933
+ return chunks.join("");
1675
1934
  }
1676
1935
  getExtensions() {
1677
1936
  return Array.from(this.extensions.keys());
@@ -1685,25 +1944,50 @@ var ChangerawrMarkdown = class {
1685
1944
  getDebugInfo() {
1686
1945
  return {
1687
1946
  warnings: this.getWarnings(),
1688
- parseTime: 0,
1689
- renderTime: 0,
1690
- tokenCount: 0,
1947
+ parseTime: this.parseTime,
1948
+ renderTime: this.renderTime,
1949
+ tokenCount: this.lastTokenCount,
1691
1950
  iterationCount: 0
1692
1951
  };
1693
1952
  }
1694
1953
  getPerformanceMetrics() {
1695
1954
  return {
1696
- parseTime: 0,
1697
- renderTime: 0,
1698
- totalTime: 0,
1699
- tokenCount: 0
1955
+ parseTime: this.parseTime,
1956
+ renderTime: this.renderTime,
1957
+ totalTime: this.parseTime + this.renderTime,
1958
+ tokenCount: this.lastTokenCount
1700
1959
  };
1701
1960
  }
1961
+ /**
1962
+ * Get cache statistics
1963
+ */
1964
+ getCacheStats() {
1965
+ return {
1966
+ parse: this.parseCache.getStats(),
1967
+ render: this.renderCache.getStats()
1968
+ };
1969
+ }
1970
+ /**
1971
+ * Clear all caches
1972
+ */
1973
+ clearCaches() {
1974
+ this.parseCache.clear();
1975
+ this.renderCache.clear();
1976
+ }
1977
+ /**
1978
+ * Update cache capacity
1979
+ */
1980
+ setCacheSize(size) {
1981
+ this.parseCache.setCapacity(size);
1982
+ this.renderCache.setCapacity(size);
1983
+ }
1702
1984
  rebuildParserAndRenderer() {
1703
1985
  const parserConfig = this.parser.getConfig();
1704
1986
  const rendererConfig = this.renderer.getConfig();
1705
1987
  this.parser = new MarkdownParser(parserConfig);
1706
1988
  this.renderer = new MarkdownRenderer(rendererConfig);
1989
+ this.parseCache.clear();
1990
+ this.renderCache.clear();
1707
1991
  const extensionsToRegister = Array.from(this.extensions.values());
1708
1992
  const featureExtensions = extensionsToRegister.filter(
1709
1993
  (ext) => ["alert", "button", "embed"].includes(ext.name)
@@ -2348,6 +2632,7 @@ export {
2348
2632
  ImageExtension,
2349
2633
  InlineCodeExtension,
2350
2634
  ItalicExtension,
2635
+ LRUCache,
2351
2636
  LineBreakExtension,
2352
2637
  LinkExtension,
2353
2638
  ListExtension,
@@ -2380,10 +2665,12 @@ export {
2380
2665
  generateId,
2381
2666
  getASTStats,
2382
2667
  getTokenStats,
2668
+ hashContent,
2383
2669
  isBrowser,
2384
2670
  isNode,
2385
2671
  isValidUrl,
2386
2672
  markdown2 as markdown,
2673
+ memoize,
2387
2674
  minimalClasses,
2388
2675
  parseASTFromJSON,
2389
2676
  parseCum,