quasar-ui-danx 0.4.99 → 0.5.0

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 (34) hide show
  1. package/dist/danx.es.js +12988 -12452
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +103 -101
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +4 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Code/MarkdownContent.vue +160 -6
  9. package/src/helpers/formats/index.ts +1 -1
  10. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  11. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  12. package/src/helpers/formats/markdown/index.ts +85 -0
  13. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  14. package/src/helpers/formats/markdown/render/index.ts +92 -0
  15. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  16. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  17. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  18. package/src/helpers/formats/markdown/state.ts +58 -0
  19. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  20. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  21. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  22. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  23. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  24. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  25. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  26. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  27. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  28. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  29. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  30. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  31. package/src/helpers/formats/markdown/types.ts +63 -0
  32. package/src/styles/danx.scss +1 -0
  33. package/src/styles/themes/danx/markdown.scss +96 -0
  34. package/src/helpers/formats/renderMarkdown.ts +0 -338
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.99",
3
+ "version": "0.5.0",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -15,7 +15,9 @@
15
15
  "build:bundle": "vite build",
16
16
  "build": "yarn clean && yarn build:types && yarn build:bundle",
17
17
  "preview": "vite preview",
18
- "postversion": "yarn build && npm publish && cd .. && git add ui && git commit -m \"v$npm_package_version\" && git tag \"v$npm_package_version\" && git push"
18
+ "publish:patch": "./scripts/publish.sh patch",
19
+ "publish:minor": "./scripts/publish.sh minor",
20
+ "publish:major": "./scripts/publish.sh major"
19
21
  },
20
22
  "repository": {
21
23
  "type": "git",
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # NOTE: npm now requires granular tokens with 2FA, max 90-day lifetime
5
+ # Classic tokens have been revoked. If auth fails, you'll need to:
6
+ # 1. Run `npm login` to create a new granular token
7
+ # 2. Complete 2FA verification
8
+ # See: https://gh.io/all-npm-classic-tokens-revoked
9
+
10
+ # Colors for output
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ NC='\033[0m' # No Color
15
+
16
+ echo -e "${YELLOW}Checking npm authentication...${NC}"
17
+
18
+ # Check if logged in by trying to get current user
19
+ if ! npm whoami &>/dev/null; then
20
+ echo -e "${RED}Not logged in to npm or token expired.${NC}"
21
+ echo -e "${YELLOW}Please log in to npm:${NC}"
22
+ npm login
23
+
24
+ # Verify login succeeded
25
+ if ! npm whoami &>/dev/null; then
26
+ echo -e "${RED}Login failed. Aborting.${NC}"
27
+ exit 1
28
+ fi
29
+ fi
30
+
31
+ CURRENT_USER=$(npm whoami)
32
+ echo -e "${GREEN}Logged in as: ${CURRENT_USER}${NC}"
33
+
34
+ # Get the version bump type (default to patch)
35
+ BUMP_TYPE=${1:-patch}
36
+
37
+ if [[ ! "$BUMP_TYPE" =~ ^(patch|minor|major)$ ]]; then
38
+ echo -e "${RED}Invalid version bump type: ${BUMP_TYPE}${NC}"
39
+ echo "Usage: ./scripts/publish.sh [patch|minor|major]"
40
+ exit 1
41
+ fi
42
+
43
+ echo -e "${YELLOW}Bumping version (${BUMP_TYPE})...${NC}"
44
+
45
+ # Build first to catch any errors before version bump
46
+ echo -e "${YELLOW}Building...${NC}"
47
+ yarn build
48
+
49
+ # Bump version (this updates package.json)
50
+ npm version $BUMP_TYPE --no-git-tag-version
51
+
52
+ # Get the new version
53
+ NEW_VERSION=$(node -p "require('./package.json').version")
54
+ echo -e "${GREEN}New version: ${NEW_VERSION}${NC}"
55
+
56
+ # Publish to npm
57
+ echo -e "${YELLOW}Publishing to npm...${NC}"
58
+ if npm publish; then
59
+ echo -e "${GREEN}Published successfully!${NC}"
60
+
61
+ # Git operations
62
+ echo -e "${YELLOW}Committing and tagging...${NC}"
63
+ cd ..
64
+ git add ui
65
+ git commit -m "v${NEW_VERSION}"
66
+ git tag "v${NEW_VERSION}"
67
+ git push
68
+ git push --tags
69
+
70
+ echo -e "${GREEN}Done! Published v${NEW_VERSION}${NC}"
71
+ else
72
+ echo -e "${RED}Publish failed!${NC}"
73
+ echo -e "${YELLOW}Rolling back version in package.json...${NC}"
74
+ git checkout package.json
75
+ exit 1
76
+ fi
@@ -44,8 +44,29 @@
44
44
  <li
45
45
  v-for="(item, itemIndex) in token.items"
46
46
  :key="itemIndex"
47
- v-html="parseInlineContent(item)"
48
- />
47
+ >
48
+ <span v-html="parseInlineContent(item.content)" />
49
+ <template v-if="item.children && item.children.length > 0">
50
+ <template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
51
+ <!-- Nested unordered list -->
52
+ <ul v-if="child.type === 'ul'">
53
+ <li
54
+ v-for="(nestedItem, nestedIndex) in child.items"
55
+ :key="nestedIndex"
56
+ v-html="renderListItem(nestedItem)"
57
+ />
58
+ </ul>
59
+ <!-- Nested ordered list -->
60
+ <ol v-else-if="child.type === 'ol'" :start="child.start">
61
+ <li
62
+ v-for="(nestedItem, nestedIndex) in child.items"
63
+ :key="nestedIndex"
64
+ v-html="renderListItem(nestedItem)"
65
+ />
66
+ </ol>
67
+ </template>
68
+ </template>
69
+ </li>
49
70
  </ul>
50
71
 
51
72
  <!-- Ordered lists -->
@@ -56,10 +77,86 @@
56
77
  <li
57
78
  v-for="(item, itemIndex) in token.items"
58
79
  :key="itemIndex"
59
- v-html="parseInlineContent(item)"
60
- />
80
+ >
81
+ <span v-html="parseInlineContent(item.content)" />
82
+ <template v-if="item.children && item.children.length > 0">
83
+ <template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
84
+ <!-- Nested unordered list -->
85
+ <ul v-if="child.type === 'ul'">
86
+ <li
87
+ v-for="(nestedItem, nestedIndex) in child.items"
88
+ :key="nestedIndex"
89
+ v-html="renderListItem(nestedItem)"
90
+ />
91
+ </ul>
92
+ <!-- Nested ordered list -->
93
+ <ol v-else-if="child.type === 'ol'" :start="child.start">
94
+ <li
95
+ v-for="(nestedItem, nestedIndex) in child.items"
96
+ :key="nestedIndex"
97
+ v-html="renderListItem(nestedItem)"
98
+ />
99
+ </ol>
100
+ </template>
101
+ </template>
102
+ </li>
61
103
  </ol>
62
104
 
105
+ <!-- Task lists -->
106
+ <ul
107
+ v-else-if="token.type === 'task_list'"
108
+ class="task-list"
109
+ >
110
+ <li
111
+ v-for="(item, itemIndex) in token.items"
112
+ :key="itemIndex"
113
+ class="task-list-item"
114
+ >
115
+ <input
116
+ type="checkbox"
117
+ :checked="item.checked"
118
+ disabled
119
+ />
120
+ <span v-html="parseInlineContent(item.content)" />
121
+ </li>
122
+ </ul>
123
+
124
+ <!-- Tables -->
125
+ <table v-else-if="token.type === 'table'">
126
+ <thead>
127
+ <tr>
128
+ <th
129
+ v-for="(header, hIndex) in token.headers"
130
+ :key="hIndex"
131
+ :style="token.alignments[hIndex] ? { textAlign: token.alignments[hIndex] } : {}"
132
+ v-html="parseInlineContent(header)"
133
+ />
134
+ </tr>
135
+ </thead>
136
+ <tbody>
137
+ <tr v-for="(row, rIndex) in token.rows" :key="rIndex">
138
+ <td
139
+ v-for="(cell, cIndex) in row"
140
+ :key="cIndex"
141
+ :style="token.alignments[cIndex] ? { textAlign: token.alignments[cIndex] } : {}"
142
+ v-html="parseInlineContent(cell)"
143
+ />
144
+ </tr>
145
+ </tbody>
146
+ </table>
147
+
148
+ <!-- Definition lists -->
149
+ <dl v-else-if="token.type === 'dl'">
150
+ <template v-for="(item, itemIndex) in token.items" :key="itemIndex">
151
+ <dt v-html="parseInlineContent(item.term)" />
152
+ <dd
153
+ v-for="(def, defIndex) in item.definitions"
154
+ :key="'def-' + defIndex"
155
+ v-html="parseInlineContent(def)"
156
+ />
157
+ </template>
158
+ </dl>
159
+
63
160
  <!-- Horizontal rules -->
64
161
  <hr v-else-if="token.type === 'hr'" />
65
162
 
@@ -69,13 +166,30 @@
69
166
  v-html="parseInlineContent(token.content).replace(/\n/g, '<br />')"
70
167
  />
71
168
  </template>
169
+
170
+ <!-- Footnotes section -->
171
+ <section v-if="hasFootnotes" class="footnotes">
172
+ <hr />
173
+ <ol class="footnote-list">
174
+ <li
175
+ v-for="fn in sortedFootnotes"
176
+ :key="fn.id"
177
+ :id="'fn-' + fn.id"
178
+ class="footnote-item"
179
+ >
180
+ <span v-html="parseInlineContent(fn.content)" />
181
+ <a :href="'#fnref-' + fn.id" class="footnote-backref">&#8617;</a>
182
+ </li>
183
+ </ol>
184
+ </section>
72
185
  </div>
73
186
  </template>
74
187
 
75
188
  <script setup lang="ts">
76
189
  import { computed, reactive } from "vue";
77
190
  import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
78
- import { tokenizeBlocks, parseInline, renderMarkdown, BlockToken } from "../../../helpers/formats/renderMarkdown";
191
+ import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
192
+ import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
79
193
  import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
80
194
  import LanguageBadge from "./LanguageBadge.vue";
81
195
 
@@ -96,9 +210,30 @@ const convertedContent = reactive<Record<number, string>>({});
96
210
  // Tokenize the markdown content
97
211
  const tokens = computed<BlockToken[]>(() => {
98
212
  if (!props.content) return [];
213
+ // Reset parser state before tokenizing to clear refs from previous content
214
+ // This ensures link refs and footnotes are freshly parsed for this content
215
+ resetParserState();
99
216
  return tokenizeBlocks(props.content);
100
217
  });
101
218
 
219
+ // Computed properties for footnotes
220
+ // IMPORTANT: Access tokens.value to ensure tokenizeBlocks runs first,
221
+ // which populates currentFootnotes and currentLinkRefs
222
+ const footnotes = computed(() => {
223
+ // Force dependency on tokens - this ensures tokenizeBlocks has run
224
+ // and populated the module-level currentFootnotes before we read it
225
+ tokens.value;
226
+ return getFootnotes();
227
+ });
228
+
229
+ const hasFootnotes = computed(() => Object.keys(footnotes.value).length > 0);
230
+
231
+ const sortedFootnotes = computed(() => {
232
+ return Object.entries(footnotes.value)
233
+ .sort((a, b) => a[1].index - b[1].index)
234
+ .map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
235
+ });
236
+
102
237
  // Check if a language is toggleable (json or yaml)
103
238
  function isToggleableLanguage(language: string): boolean {
104
239
  if (!language) return false;
@@ -222,9 +357,28 @@ function parseInlineContent(text: string): string {
222
357
  return parseInline(text, true);
223
358
  }
224
359
 
360
+ // Render a list item with potential nested children
361
+ function renderListItem(item: ListItem): string {
362
+ let html = parseInline(item.content, true);
363
+ if (item.children && item.children.length > 0) {
364
+ for (const child of item.children) {
365
+ if (child.type === "ul") {
366
+ const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
367
+ html += `<ul>${items}</ul>`;
368
+ } else if (child.type === "ol") {
369
+ const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
370
+ const startAttr = child.start !== 1 ? ` start="${child.start}"` : "";
371
+ html += `<ol${startAttr}>${items}</ol>`;
372
+ }
373
+ }
374
+ }
375
+ return html;
376
+ }
377
+
225
378
  // Render blockquote content (can contain nested markdown)
379
+ // Use preserveState to keep link refs and footnotes from parent document
226
380
  function renderBlockquote(content: string): string {
227
- return renderMarkdown(content);
381
+ return renderMarkdown(content, { preserveState: true });
228
382
  }
229
383
  </script>
230
384
 
@@ -2,5 +2,5 @@ export * from "./datetime";
2
2
  export * from "./highlightSyntax";
3
3
  export * from "./numbers";
4
4
  export * from "./parsers";
5
- export * from "./renderMarkdown";
5
+ export * from "./markdown";
6
6
  export * from "./strings";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HTML entity escaping for XSS prevention
3
+ */
4
+
5
+ /**
6
+ * Escape HTML entities to prevent XSS
7
+ */
8
+ export function escapeHtml(text: string): string {
9
+ return text
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;")
14
+ .replace(/'/g, "&#039;");
15
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Markdown escape sequence handling
3
+ * Maps backslash-escaped characters to Unicode placeholders and back
4
+ * Using Private Use Area characters (U+E000-U+F8FF) to avoid conflicts
5
+ */
6
+
7
+ /**
8
+ * Escape sequences mapping - character to Unicode placeholder
9
+ */
10
+ export const ESCAPE_MAP: Record<string, string> = {
11
+ "\\*": "\uE000",
12
+ "\\_": "\uE001",
13
+ "\\~": "\uE002",
14
+ "\\`": "\uE003",
15
+ "\\[": "\uE004",
16
+ "\\]": "\uE005",
17
+ "\\#": "\uE006",
18
+ "\\&gt;": "\uE007", // Escaped > becomes &gt; after HTML escaping
19
+ "\\-": "\uE008",
20
+ "\\+": "\uE009",
21
+ "\\.": "\uE00A",
22
+ "\\!": "\uE00B",
23
+ "\\=": "\uE00C",
24
+ "\\^": "\uE00D"
25
+ };
26
+
27
+ /**
28
+ * Reverse mapping - placeholder back to literal character
29
+ * Generated from ESCAPE_MAP for DRY compliance
30
+ */
31
+ export const UNESCAPE_MAP: Record<string, string> = Object.fromEntries(
32
+ Object.entries(ESCAPE_MAP).map(([escaped, placeholder]) => {
33
+ // Extract the literal character from the escape sequence
34
+ // "\\*" -> "*", "\\&gt;" -> "&gt;" (special case for HTML-escaped >)
35
+ const literal = escaped.startsWith("\\&") ? escaped.slice(1) : escaped.slice(1);
36
+ return [placeholder, literal];
37
+ })
38
+ );
39
+
40
+ /**
41
+ * Apply escape sequences - convert backslash-escaped characters to placeholders
42
+ */
43
+ export function applyEscapes(text: string): string {
44
+ let result = text;
45
+ for (const [pattern, placeholder] of Object.entries(ESCAPE_MAP)) {
46
+ result = result.split(pattern).join(placeholder);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Revert escape sequences - convert placeholders back to literal characters
53
+ */
54
+ export function revertEscapes(text: string): string {
55
+ let result = text;
56
+ for (const [placeholder, literal] of Object.entries(UNESCAPE_MAP)) {
57
+ result = result.split(placeholder).join(literal);
58
+ }
59
+ return result;
60
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lightweight markdown to HTML renderer
3
+ * Zero external dependencies, XSS-safe by default
4
+ *
5
+ * Supports:
6
+ * - Headings (# through ###### and setext-style with === or ---)
7
+ * - Paragraphs (double newlines)
8
+ * - Code blocks (```language ... ``` and indented with 4 spaces or tab)
9
+ * - Blockquotes (> text)
10
+ * - Unordered lists (-, *, +)
11
+ * - Ordered lists (1., 2., etc.)
12
+ * - Task lists (- [ ] unchecked, - [x] checked)
13
+ * - Definition lists (Term followed by : Definition)
14
+ * - Tables (| col | col | with alignment support)
15
+ * - Horizontal rules (---, ***, ___)
16
+ * - Bold (**text** or __text__)
17
+ * - Italic (*text* or _text_)
18
+ * - Bold+Italic (***text***)
19
+ * - Inline code (`code`)
20
+ * - Links [text](url)
21
+ * - Reference-style links ([text][ref], [text][], [ref] with [ref]: url definitions)
22
+ * - Images ![alt](url)
23
+ * - Escape sequences (\* \_ \~ etc.)
24
+ * - Hard line breaks (two trailing spaces)
25
+ * - Autolinks (<https://...> and <email@...>)
26
+ * - Strikethrough (~~text~~)
27
+ * - Highlight (==text==)
28
+ * - Superscript (X^2^)
29
+ * - Subscript (H~2~O)
30
+ * - Footnotes ([^id] with [^id]: content definitions)
31
+ */
32
+
33
+ // Re-export types
34
+ export type {
35
+ MarkdownRenderOptions,
36
+ LinkReference,
37
+ FootnoteDefinition,
38
+ ListItem,
39
+ BlockToken,
40
+ TableAlignment,
41
+ ParseResult
42
+ } from "./types";
43
+
44
+ // Re-export state management
45
+ export { getFootnotes, resetParserState } from "./state";
46
+
47
+ // Re-export parsers
48
+ export { parseInline } from "./parseInline";
49
+ export { tokenizeBlocks } from "./tokenize";
50
+
51
+ // Re-export renderers
52
+ export { renderTokens } from "./render";
53
+
54
+ // Import for main function
55
+ import type { MarkdownRenderOptions } from "./types";
56
+ import { getFootnotes, resetParserState } from "./state";
57
+ import { tokenizeBlocks } from "./tokenize";
58
+ import { renderTokens } from "./render";
59
+ import { renderFootnotesSection } from "./render/renderFootnotes";
60
+
61
+ /**
62
+ * Convert markdown text to HTML
63
+ */
64
+ export function renderMarkdown(markdown: string, options?: MarkdownRenderOptions): string {
65
+ if (!markdown) return "";
66
+
67
+ const sanitize = options?.sanitize ?? true;
68
+ const preserveState = options?.preserveState ?? false;
69
+
70
+ // Reset state for fresh document parse (unless preserving for nested rendering)
71
+ if (!preserveState) {
72
+ resetParserState();
73
+ }
74
+
75
+ const tokens = tokenizeBlocks(markdown);
76
+ let html = renderTokens(tokens, sanitize);
77
+
78
+ // Append footnotes section if any exist (only for top-level rendering)
79
+ const footnotes = getFootnotes();
80
+ if (!preserveState && Object.keys(footnotes).length > 0) {
81
+ html += renderFootnotesSection(footnotes, sanitize);
82
+ }
83
+
84
+ return html;
85
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Inline markdown element parser
3
+ * Handles: bold, italic, strikethrough, highlight, superscript, subscript,
4
+ * links, images, reference-style links, footnotes, autolinks, code
5
+ */
6
+
7
+ import { escapeHtml } from "./escapeHtml";
8
+ import { applyEscapes, revertEscapes } from "./escapeSequences";
9
+ import { getLinkRefs, getFootnotes } from "./state";
10
+
11
+ /**
12
+ * Parse inline markdown elements within text
13
+ * Order matters: more specific patterns first
14
+ */
15
+ export function parseInline(text: string, sanitize: boolean = true): string {
16
+ if (!text) return "";
17
+
18
+ const currentLinkRefs = getLinkRefs();
19
+ const currentFootnotes = getFootnotes();
20
+
21
+ // Escape HTML if sanitizing (before applying markdown)
22
+ let result = sanitize ? escapeHtml(text) : text;
23
+
24
+ // 1. ESCAPE SEQUENCES - Pre-process backslash escapes to placeholders
25
+ // Must be first so escaped characters aren't treated as markdown
26
+ result = applyEscapes(result);
27
+
28
+ // 2. HARD LINE BREAKS - Two trailing spaces + newline becomes <br />
29
+ result = result.replace(/ {2,}\n/g, "<br />\n");
30
+
31
+ // 3. AUTOLINKS - Must be before regular link parsing
32
+ // URL autolinks: <https://example.com>
33
+ result = result.replace(/&lt;(https?:\/\/[^&]+)&gt;/g, '<a href="$1">$1</a>');
34
+ // Email autolinks: <user@example.com>
35
+ result = result.replace(/&lt;([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})&gt;/g, '<a href="mailto:$1">$1</a>');
36
+
37
+ // 4. FOOTNOTE REFERENCES: [^id]
38
+ // Must be before regular link/image parsing to avoid conflicts
39
+ result = result.replace(/\[\^([^\]]+)\]/g, (match, fnId) => {
40
+ const fn = currentFootnotes[fnId];
41
+ if (fn) {
42
+ return `<sup class="footnote-ref"><a href="#fn-${fnId}" id="fnref-${fnId}">[${fn.index}]</a></sup>`;
43
+ }
44
+ return match; // Keep original if footnote not defined
45
+ });
46
+
47
+ // 5. IMAGES: ![alt](url) - must be before links
48
+ result = result.replace(
49
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
50
+ '<img src="$2" alt="$1" />'
51
+ );
52
+
53
+ // 6. INLINE LINKS: [text](url)
54
+ result = result.replace(
55
+ /\[([^\]]+)\]\(([^)]+)\)/g,
56
+ '<a href="$2">$1</a>'
57
+ );
58
+
59
+ // 7. REFERENCE-STYLE LINKS - Process after regular links
60
+ // Full reference: [text][ref-id]
61
+ result = result.replace(/\[([^\]]+)\]\[([^\]]+)\]/g, (match, text, refId) => {
62
+ const ref = currentLinkRefs[refId.toLowerCase()];
63
+ if (ref) {
64
+ const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
65
+ return `<a href="${ref.url}"${title}>${text}</a>`;
66
+ }
67
+ return match; // Keep original if ref not found
68
+ });
69
+
70
+ // Collapsed reference: [text][]
71
+ result = result.replace(/\[([^\]]+)\]\[\]/g, (match, text) => {
72
+ const ref = currentLinkRefs[text.toLowerCase()];
73
+ if (ref) {
74
+ const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
75
+ return `<a href="${ref.url}"${title}>${text}</a>`;
76
+ }
77
+ return match;
78
+ });
79
+
80
+ // Shortcut reference: [ref] alone (must not match [text](url) or [text][ref])
81
+ // Only match [word] not followed by ( or [
82
+ result = result.replace(/\[([^\]]+)\](?!\(|\[)/g, (match, text) => {
83
+ const ref = currentLinkRefs[text.toLowerCase()];
84
+ if (ref) {
85
+ const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
86
+ return `<a href="${ref.url}"${title}>${text}</a>`;
87
+ }
88
+ return match;
89
+ });
90
+
91
+ // 8. INLINE CODE: `code`
92
+ result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
93
+
94
+ // 9. STRIKETHROUGH: ~~text~~ - Must be before subscript (single tilde)
95
+ result = result.replace(/~~([^~]+)~~/g, "<del>$1</del>");
96
+
97
+ // 10. HIGHLIGHT: ==text==
98
+ result = result.replace(/==([^=]+)==/g, "<mark>$1</mark>");
99
+
100
+ // 11. SUPERSCRIPT: X^2^ - Must be before subscript
101
+ result = result.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
102
+
103
+ // 12. SUBSCRIPT: H~2~O - Single tilde, use negative lookbehind/lookahead to avoid ~~
104
+ result = result.replace(/(?<!~)~([^~]+)~(?!~)/g, "<sub>$1</sub>");
105
+
106
+ // 13. BOLD + ITALIC: ***text*** or ___text___
107
+ result = result.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
108
+ result = result.replace(/___([^_]+)___/g, "<strong><em>$1</em></strong>");
109
+
110
+ // 14. BOLD: **text** or __text__
111
+ result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
112
+ result = result.replace(/__([^_]+)__/g, "<strong>$1</strong>");
113
+
114
+ // 15. ITALIC: *text* or _text_ (but not inside words for underscores)
115
+ // For asterisks, match any single asterisk pairs
116
+ result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
117
+ // For underscores, only match at word boundaries
118
+ result = result.replace(/(^|[^a-zA-Z0-9])_([^_]+)_([^a-zA-Z0-9]|$)/g, "$1<em>$2</em>$3");
119
+
120
+ // LAST: Restore escaped characters from placeholders to literals
121
+ result = revertEscapes(result);
122
+
123
+ return result;
124
+ }