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.
- package/dist/danx.es.js +12988 -12452
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +103 -101
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Code/MarkdownContent.vue +160 -6
- package/src/helpers/formats/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/index.ts +85 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -0
- 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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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">↩</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,
|
|
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
|
|
|
@@ -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, "&")
|
|
11
|
+
.replace(/</g, "<")
|
|
12
|
+
.replace(/>/g, ">")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/'/g, "'");
|
|
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
|
+
"\\>": "\uE007", // Escaped > becomes > 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
|
+
// "\\*" -> "*", "\\>" -> ">" (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 
|
|
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(/<(https?:\/\/[^&]+)>/g, '<a href="$1">$1</a>');
|
|
34
|
+
// Email autolinks: <user@example.com>
|
|
35
|
+
result = result.replace(/<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/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:  - 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
|
+
}
|