@humanspeak/svelte-markdown 0.8.7 → 0.8.9

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/README.md CHANGED
@@ -74,19 +74,16 @@ This package carefully selects its dependencies to provide a robust and maintain
74
74
  ### Core Dependencies
75
75
 
76
76
  - **marked**
77
-
78
77
  - Industry-standard markdown parser
79
78
  - Battle-tested in production
80
79
  - Extensive security features
81
80
 
82
81
  - **github-slugger**
83
-
84
82
  - GitHub-style heading ID generation
85
83
  - Unicode support
86
84
  - Collision handling
87
85
 
88
86
  - **htmlparser2**
89
-
90
87
  - High-performance HTML parsing
91
88
  - Streaming capabilities
92
89
  - Security-focused design
@@ -84,73 +84,57 @@
84
84
  {/if}
85
85
  {:else if type in renderers}
86
86
  {#if type === 'table'}
87
- <renderers.table {...rest}>
88
- <renderers.tablehead {...rest}>
89
- <renderers.tablerow {...rest}>
90
- {#each header ?? [] as headerItem, i (i)}
91
- {@const { align: _align, ...cellRest } = rest}
92
- <renderers.tablecell
93
- header={true}
94
- align={(rest.align as string[])[i]}
95
- {...cellRest}
96
- >
97
- <Parser tokens={headerItem.tokens} {renderers} />
98
- </renderers.tablecell>
99
- {/each}
100
- </renderers.tablerow>
101
- </renderers.tablehead>
102
- <renderers.tablebody {...rest}>
103
- {#each rows ?? [] as row, i (i)}
104
- <renderers.tablerow {...rest}>
105
- {#each row ?? [] as cells, i (i)}
106
- {@const { align: _align, ...cellRest } = rest}
107
- <renderers.tablecell
108
- header={false}
109
- align={(rest.align as string[])[i]}
110
- {...cellRest}
111
- >
112
- {#if cells.tokens?.[0]?.type === 'html'}
113
- {@const token = cells.tokens[0] as Token & {
114
- tag: string
115
- tokens?: Token[]
116
- }}
117
- {@const { tag, ...localRest } = token}
118
- {@const htmlTag = tag as keyof typeof Html}
119
- {#if renderers.html && htmlTag in renderers.html}
120
- {@const HtmlComponent =
121
- renderers.html[htmlTag as keyof typeof renderers.html]}
122
- <HtmlComponent {...token}>
123
- {#if token.tokens?.length}
124
- <Parser
125
- tokens={token.tokens}
126
- {renderers}
127
- {...Object.fromEntries(
128
- Object.entries(localRest).filter(
129
- ([key]) => key !== 'attributes'
130
- )
131
- )}
132
- />
133
- {/if}
134
- </HtmlComponent>
135
- {/if}
136
- {:else}
137
- <Parser tokens={cells.tokens} {renderers} />
138
- {/if}
139
- </renderers.tablecell>
87
+ {#if renderers.table && renderers.tablerow && renderers.tablecell}
88
+ <renderers.table {...rest}>
89
+ {#if renderers.tablehead}
90
+ <renderers.tablehead {...rest}>
91
+ <renderers.tablerow {...rest}>
92
+ {#each header ?? [] as headerItem, i (i)}
93
+ {@const { align: _align, ...cellRest } = rest}
94
+ <renderers.tablecell
95
+ header={true}
96
+ align={(rest.align as string[])[i]}
97
+ {...cellRest}
98
+ >
99
+ <Parser tokens={headerItem.tokens} {renderers} />
100
+ </renderers.tablecell>
101
+ {/each}
102
+ </renderers.tablerow>
103
+ </renderers.tablehead>
104
+ {/if}
105
+ {#if renderers.tablebody}
106
+ <renderers.tablebody {...rest}>
107
+ {#each rows ?? [] as row, i (i)}
108
+ <renderers.tablerow {...rest}>
109
+ {#each row ?? [] as cells, i (i)}
110
+ {@const { align: _align, ...cellRest } = rest}
111
+ <renderers.tablecell
112
+ {...cellRest}
113
+ header={false}
114
+ align={(rest.align as string[])[i]}
115
+ >
116
+ {#each cells.tokens ?? [] as cellToken, index (index)}
117
+ <Parser {...cellRest} {...cellToken} {renderers} />
118
+ {/each}
119
+ </renderers.tablecell>
120
+ {/each}
121
+ </renderers.tablerow>
140
122
  {/each}
141
- </renderers.tablerow>
142
- {/each}
143
- </renderers.tablebody>
144
- </renderers.table>
145
- {:else if type === 'list'}
123
+ </renderers.tablebody>
124
+ {/if}
125
+ </renderers.table>
126
+ {/if}
127
+ {:else if type === 'list' && renderers.list}
146
128
  {#if ordered}
147
129
  <renderers.list {ordered} {...rest}>
148
130
  {@const { items, ...parserRest }: {items: Props[]} = rest}
149
131
  {#each items as item, index (index)}
150
132
  {@const OrderedListComponent = renderers.orderedlistitem || renderers.listitem}
151
- <OrderedListComponent {...item}>
152
- <Parser {...parserRest} tokens={item.tokens} {renderers} />
153
- </OrderedListComponent>
133
+ {#if OrderedListComponent}
134
+ <OrderedListComponent {...item}>
135
+ <Parser {...parserRest} tokens={item.tokens} {renderers} />
136
+ </OrderedListComponent>
137
+ {/if}
154
138
  {/each}
155
139
  </renderers.list>
156
140
  {:else}
@@ -159,9 +143,11 @@
159
143
  {#each items as item, index (index)}
160
144
  {@const UnorderedListComponent =
161
145
  renderers.unorderedlistitem || renderers.listitem}
162
- <UnorderedListComponent {...item}>
163
- <Parser {...parserRest} tokens={item.tokens} {renderers} />
164
- </UnorderedListComponent>
146
+ {#if UnorderedListComponent}
147
+ <UnorderedListComponent {...item}>
148
+ <Parser {...parserRest} tokens={item.tokens} {renderers} />
149
+ </UnorderedListComponent>
150
+ {/if}
165
151
  {/each}
166
152
  </renderers.list>
167
153
  {/if}
@@ -170,18 +156,20 @@
170
156
  {@const htmlTag = rest.tag as keyof typeof Html}
171
157
  {#if renderers.html && htmlTag in renderers.html}
172
158
  {@const HtmlComponent = renderers.html[htmlTag as keyof typeof renderers.html]}
173
- {@const tokens = (rest.tokens as Token[]) ?? ([] as Token[])}
174
- <HtmlComponent {...rest}>
175
- {#if tokens.length}
176
- <Parser
177
- {tokens}
178
- {renderers}
179
- {...Object.fromEntries(
180
- Object.entries(localRest).filter(([key]) => key !== 'attributes')
181
- )}
182
- />
183
- {/if}
184
- </HtmlComponent>
159
+ {#if HtmlComponent}
160
+ {@const tokens = (rest.tokens as Token[]) ?? ([] as Token[])}
161
+ <HtmlComponent {...rest}>
162
+ {#if tokens.length}
163
+ <Parser
164
+ {tokens}
165
+ {renderers}
166
+ {...Object.fromEntries(
167
+ Object.entries(localRest).filter(([key]) => key !== 'attributes')
168
+ )}
169
+ />
170
+ {/if}
171
+ </HtmlComponent>
172
+ {/if}
185
173
  {:else}
186
174
  <Parser
187
175
  tokens={(rest.tokens as Token[]) ?? ([] as Token[])}
@@ -191,13 +179,15 @@
191
179
  {/if}
192
180
  {:else}
193
181
  {@const GeneralComponent = renderers[type as keyof typeof renderers] as RendererComponent}
194
- <GeneralComponent {...rest}>
195
- {#if tokens}
196
- {@const { text: _text, raw: _raw, ...parserRest } = rest}
197
- <Parser {...parserRest} {tokens} {renderers} />
198
- {:else}
199
- <renderers.rawtext text={rest.raw} />
200
- {/if}
201
- </GeneralComponent>
182
+ {#if GeneralComponent}
183
+ <GeneralComponent {...rest}>
184
+ {#if tokens}
185
+ {@const { text: _text, raw: _raw, ...parserRest } = rest}
186
+ <Parser {...parserRest} {tokens} {renderers} />
187
+ {:else}
188
+ <renderers.rawtext text={rest.raw} {...rest} />
189
+ {/if}
190
+ </GeneralComponent>
191
+ {/if}
202
192
  {/if}
203
193
  {/if}
@@ -0,0 +1,9 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ attributes?: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
4
+ }
5
+
6
+ const { attributes = {} }: Props = $props()
7
+ </script>
8
+
9
+ <br {...attributes} />
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ attributes?: Record<string, any>;
3
+ }
4
+ declare const Br: import("svelte").Component<Props, {}, "">;
5
+ type Br = ReturnType<typeof Br>;
6
+ export default Br;
@@ -8,6 +8,7 @@ import B from './B.svelte';
8
8
  import Bdi from './Bdi.svelte';
9
9
  import Bdo from './Bdo.svelte';
10
10
  import Blockquote from './Blockquote.svelte';
11
+ import Br from './Br.svelte';
11
12
  import Button from './Button.svelte';
12
13
  import Canvas from './Canvas.svelte';
13
14
  import Cite from './Cite.svelte';
@@ -91,6 +92,7 @@ export const Html = {
91
92
  bdi: Bdi,
92
93
  bdo: Bdo,
93
94
  blockquote: Blockquote,
95
+ br: Br,
94
96
  button: Button,
95
97
  canvas: Canvas,
96
98
  cite: Cite,
@@ -94,6 +94,7 @@ export declare const containsMultipleTags: (html: string) => boolean;
94
94
  *
95
95
  * Key features:
96
96
  * - Breaks down complex HTML structures into atomic tokens
97
+ * - Formats self-closing tags with proper syntax (e.g., <br> -> <br/>)
97
98
  * - Maintains attribute information
98
99
  * - Preserves proper nesting relationships
99
100
  * - Handles malformed HTML gracefully
@@ -12,6 +12,11 @@ import * as htmlparser2 from 'htmlparser2';
12
12
  */
13
13
  const HTML_TAG_PATTERN = /<\/?([a-zA-Z][a-zA-Z0-9-]{0,})(?:\s+[^>]*)?>/;
14
14
  const htmlTagRegex = new RegExp(HTML_TAG_PATTERN);
15
+ /**
16
+ * Regex pattern for self-closing HTML tags.
17
+ * @const {RegExp}
18
+ */
19
+ const SELF_CLOSING_TAGS = /^(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)$/i;
15
20
  /**
16
21
  * Analyzes a string to determine if it contains an HTML tag and its characteristics.
17
22
  *
@@ -37,6 +42,33 @@ export const isHtmlOpenTag = (raw) => {
37
42
  return null;
38
43
  return { tag: match[1], isOpening: !raw.startsWith('</') };
39
44
  };
45
+ /**
46
+ * Formats individual HTML tokens to ensure self-closing tags are properly formatted.
47
+ * This handles cases like <br> -> <br/> without affecting the token structure.
48
+ *
49
+ * @param {Token} token - HTML token to format
50
+ * @returns {Token} Formatted token with proper self-closing syntax
51
+ */
52
+ const formatSelfClosingHtmlToken = (token) => {
53
+ // Extract tag name from raw HTML
54
+ const tagMatch = token.raw.match(/<\/?([a-zA-Z][a-zA-Z0-9-]*)/i);
55
+ if (!tagMatch)
56
+ return token;
57
+ const tagName = tagMatch[1];
58
+ if (!SELF_CLOSING_TAGS.test(tagName))
59
+ return token;
60
+ // If it's a self-closing tag and doesn't already end with />, format it properly
61
+ if (!token.raw.endsWith('/>')) {
62
+ const formattedRaw = token.raw.replace(/\s*>$/, '/>');
63
+ return {
64
+ ...token,
65
+ raw: formattedRaw,
66
+ tag: tagName,
67
+ attributes: extractAttributes(token.raw)
68
+ };
69
+ }
70
+ return token;
71
+ };
40
72
  /**
41
73
  * Parses HTML attributes from a tag string into a structured object.
42
74
  * Handles both single and double quoted attributes.
@@ -117,7 +149,6 @@ export const extractAttributes = (raw) => {
117
149
  export const parseHtmlBlock = (html) => {
118
150
  const tokens = [];
119
151
  let currentText = '';
120
- const selfClosingTags = /^(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)$/i;
121
152
  const openTags = [];
122
153
  const parser = new htmlparser2.Parser({
123
154
  onopentag: (name, attributes) => {
@@ -129,8 +160,7 @@ export const parseHtmlBlock = (html) => {
129
160
  });
130
161
  currentText = '';
131
162
  }
132
- openTags.push(name);
133
- if (selfClosingTags.test(name)) {
163
+ if (SELF_CLOSING_TAGS.test(name)) {
134
164
  tokens.push({
135
165
  type: 'html',
136
166
  raw: `<${name}${Object.entries(attributes)
@@ -141,6 +171,7 @@ export const parseHtmlBlock = (html) => {
141
171
  });
142
172
  }
143
173
  else {
174
+ openTags.push(name);
144
175
  tokens.push({
145
176
  type: 'html',
146
177
  raw: `<${name}${Object.entries(attributes)
@@ -165,7 +196,7 @@ export const parseHtmlBlock = (html) => {
165
196
  }
166
197
  // Only add closing tag if we found its opening tag
167
198
  // and it's not a self-closing tag
168
- if (openTags.includes(name) && !selfClosingTags.test(name)) {
199
+ if (openTags.includes(name) && !SELF_CLOSING_TAGS.test(name)) {
169
200
  if (html.includes(`</${name}>`)) {
170
201
  tokens.push({
171
202
  type: 'html',
@@ -219,6 +250,7 @@ export const containsMultipleTags = (html) => {
219
250
  *
220
251
  * Key features:
221
252
  * - Breaks down complex HTML structures into atomic tokens
253
+ * - Formats self-closing tags with proper syntax (e.g., <br> -> <br/>)
222
254
  * - Maintains attribute information
223
255
  * - Preserves proper nesting relationships
224
256
  * - Handles malformed HTML gracefully
@@ -251,6 +283,7 @@ export const shrinkHtmlTokens = (tokens) => {
251
283
  else if (token.type === 'table') {
252
284
  // Process header cells
253
285
  if (token.header) {
286
+ // @ts-expect-error: expected any
254
287
  token.header = token.header.map((cell) => ({
255
288
  ...cell,
256
289
  tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
@@ -258,7 +291,10 @@ export const shrinkHtmlTokens = (tokens) => {
258
291
  }
259
292
  // Process row cells
260
293
  if (token.rows) {
261
- token.rows = token.rows.map((row) => row.map((cell) => ({
294
+ // @ts-expect-error: expected any
295
+ token.rows = token.rows.map((row) =>
296
+ // @ts-expect-error: expected any
297
+ row.map((cell) => ({
262
298
  ...cell,
263
299
  tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
264
300
  })));
@@ -269,6 +305,11 @@ export const shrinkHtmlTokens = (tokens) => {
269
305
  // Parse HTML with multiple tags into separate tokens
270
306
  result.push(...parseHtmlBlock(token.raw));
271
307
  }
308
+ else if (token.type === 'html') {
309
+ // Format self-closing tags properly (e.g., <br> -> <br/>)
310
+ const formattedToken = formatSelfClosingHtmlToken(token);
311
+ result.push(formattedToken);
312
+ }
272
313
  else {
273
314
  result.push(token);
274
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-markdown",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "A powerful, customizable markdown renderer for Svelte with TypeScript support",
5
5
  "keywords": [
6
6
  "svelte",
@@ -77,47 +77,47 @@
77
77
  "dependencies": {
78
78
  "github-slugger": "^2.0.0",
79
79
  "htmlparser2": "^10.0.0",
80
- "marked": "^15.0.12"
80
+ "marked": "^16.1.1"
81
81
  },
82
82
  "devDependencies": {
83
- "@eslint/compat": "^1.2.9",
84
- "@eslint/js": "^9.27.0",
85
- "@playwright/test": "^1.52.0",
83
+ "@eslint/compat": "^1.3.1",
84
+ "@eslint/js": "^9.32.0",
85
+ "@playwright/test": "^1.54.1",
86
86
  "@sveltejs/adapter-auto": "^6.0.1",
87
- "@sveltejs/kit": "^2.21.1",
88
- "@sveltejs/package": "^2.3.11",
89
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
90
- "@testing-library/jest-dom": "^6.6.3",
87
+ "@sveltejs/kit": "^2.26.1",
88
+ "@sveltejs/package": "^2.4.0",
89
+ "@sveltejs/vite-plugin-svelte": "^6.1.0",
90
+ "@testing-library/jest-dom": "^6.6.4",
91
91
  "@testing-library/svelte": "^5.2.8",
92
92
  "@testing-library/user-event": "^14.6.1",
93
- "@types/node": "^22.15.24",
94
- "@typescript-eslint/eslint-plugin": "^8.33.0",
95
- "@typescript-eslint/parser": "^8.33.0",
96
- "@vitest/coverage-v8": "^3.1.4",
97
- "eslint": "^9.27.0",
98
- "eslint-config-prettier": "^10.1.5",
99
- "eslint-plugin-import": "^2.31.0",
100
- "eslint-plugin-svelte": "^3.9.0",
93
+ "@types/node": "^24.1.0",
94
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
95
+ "@typescript-eslint/parser": "^8.38.0",
96
+ "@vitest/coverage-v8": "^3.2.4",
97
+ "eslint": "^9.32.0",
98
+ "eslint-config-prettier": "^10.1.8",
99
+ "eslint-plugin-import": "^2.32.0",
100
+ "eslint-plugin-svelte": "^3.11.0",
101
101
  "eslint-plugin-unused-imports": "^4.1.4",
102
- "globals": "^16.2.0",
102
+ "globals": "^16.3.0",
103
103
  "jsdom": "^26.1.0",
104
- "prettier": "^3.5.3",
105
- "prettier-plugin-organize-imports": "^4.1.0",
104
+ "prettier": "^3.6.2",
105
+ "prettier-plugin-organize-imports": "^4.2.0",
106
106
  "prettier-plugin-svelte": "^3.4.0",
107
- "prettier-plugin-tailwindcss": "^0.6.11",
107
+ "prettier-plugin-tailwindcss": "^0.6.14",
108
108
  "publint": "^0.3.12",
109
- "svelte": "^5.33.4",
110
- "svelte-check": "^4.2.1",
109
+ "svelte": "^5.37.0",
110
+ "svelte-check": "^4.3.0",
111
111
  "typescript": "^5.8.3",
112
- "typescript-eslint": "^8.33.0",
113
- "vite": "^6.3.5",
114
- "vitest": "^3.1.4"
112
+ "typescript-eslint": "^8.38.0",
113
+ "vite": "^7.0.6",
114
+ "vitest": "^3.2.4"
115
115
  },
116
116
  "peerDependencies": {
117
117
  "svelte": "^5.0.0"
118
118
  },
119
119
  "volta": {
120
- "node": "22.15.1"
120
+ "node": "22.17.1"
121
121
  },
122
122
  "publishConfig": {
123
123
  "access": "public"