@humanspeak/svelte-markdown 0.7.3 → 0.7.5

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.
@@ -1,4 +1,42 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * @component Parser
4
+ *
5
+ * Recursive markdown token parser that transforms tokens into Svelte components.
6
+ * This component is the core rendering engine of the markdown system, handling
7
+ * the transformation of parsed markdown tokens into their corresponding Svelte components.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <Parser
12
+ * tokens={parsedTokens}
13
+ * renderers={customRenderers}
14
+ * type="paragraph"
15
+ * />
16
+ * ```
17
+ *
18
+ * Features:
19
+ * - Recursive token parsing
20
+ * - Custom renderer support
21
+ * - Special handling for tables, lists, and HTML content
22
+ * - Type-safe component rendering
23
+ *
24
+ * @typedef {Object} Props
25
+ * @property {string} [type] - Token type for direct component rendering
26
+ * @property {Token[] | TokensList} [tokens] - Markdown tokens to be rendered
27
+ * @property {Tokens.TableCell[]} [header] - Table header cells for table rendering
28
+ * @property {Tokens.TableCell[][]} [rows] - Table row cells for table rendering
29
+ * @property {boolean} [ordered=false] - Whether the list is ordered (for list rendering)
30
+ * @property {Renderers} renderers - Component mapping for markdown elements
31
+ *
32
+ * Implementation Notes:
33
+ * - Uses recursive rendering for nested tokens
34
+ * - Implements special logic for tables, lists, and HTML content
35
+ * - Handles component prop spreading carefully to avoid conflicts
36
+ * - Maintains type safety through TypeScript interfaces
37
+ *
38
+ */
39
+
2
40
  import Parser from './Parser.svelte'
3
41
  import Html from './renderers/html/index.js'
4
42
  import type {
@@ -44,10 +82,11 @@
44
82
  <renderers.tablehead {...rest}>
45
83
  <renderers.tablerow {...rest}>
46
84
  {#each header ?? [] as headerItem, i}
85
+ {@const { align: _align, ...cellRest } = rest}
47
86
  <renderers.tablecell
48
87
  header={true}
49
- align={(rest.align as string[])[i] || 'center'}
50
- {...rest}
88
+ align={(rest.align as string[])[i]}
89
+ {...cellRest}
51
90
  >
52
91
  <Parser tokens={headerItem.tokens} {renderers} />
53
92
  </renderers.tablecell>
@@ -58,12 +97,30 @@
58
97
  {#each rows ?? [] as row}
59
98
  <renderers.tablerow {...rest}>
60
99
  {#each row ?? [] as cells, i}
100
+ {@const { align: _align, ...cellRest } = rest}
61
101
  <renderers.tablecell
62
102
  header={false}
63
- align={(rest.align as string[])[i] ?? 'center'}
64
- {...rest}
103
+ align={(rest.align as string[])[i]}
104
+ {...cellRest}
65
105
  >
66
- <Parser tokens={cells.tokens} {renderers} />
106
+ {#if cells.type === 'html'}
107
+ {@const { tag, ...localRest } = cells}
108
+ {@const htmlTag = cells.tag as keyof typeof Html}
109
+ {#if htmlTag in Html}
110
+ {@const HtmlComponent = Html[htmlTag]}
111
+ <HtmlComponent {...cells}>
112
+ {#if cells.tokens?.length}
113
+ <Parser
114
+ tokens={cells.tokens}
115
+ {renderers}
116
+ {...localRest}
117
+ />
118
+ {/if}
119
+ </HtmlComponent>
120
+ {/if}
121
+ {:else}
122
+ <Parser tokens={cells.tokens} {renderers} />
123
+ {/if}
67
124
  </renderers.tablecell>
68
125
  {/each}
69
126
  </renderers.tablerow>
@@ -1,3 +1,40 @@
1
+ /**
2
+ * @component Parser
3
+ *
4
+ * Recursive markdown token parser that transforms tokens into Svelte components.
5
+ * This component is the core rendering engine of the markdown system, handling
6
+ * the transformation of parsed markdown tokens into their corresponding Svelte components.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <Parser
11
+ * tokens={parsedTokens}
12
+ * renderers={customRenderers}
13
+ * type="paragraph"
14
+ * />
15
+ * ```
16
+ *
17
+ * Features:
18
+ * - Recursive token parsing
19
+ * - Custom renderer support
20
+ * - Special handling for tables, lists, and HTML content
21
+ * - Type-safe component rendering
22
+ *
23
+ * @typedef {Object} Props
24
+ * @property {string} [type] - Token type for direct component rendering
25
+ * @property {Token[] | TokensList} [tokens] - Markdown tokens to be rendered
26
+ * @property {Tokens.TableCell[]} [header] - Table header cells for table rendering
27
+ * @property {Tokens.TableCell[][]} [rows] - Table row cells for table rendering
28
+ * @property {boolean} [ordered=false] - Whether the list is ordered (for list rendering)
29
+ * @property {Renderers} renderers - Component mapping for markdown elements
30
+ *
31
+ * Implementation Notes:
32
+ * - Uses recursive rendering for nested tokens
33
+ * - Implements special logic for tables, lists, and HTML content
34
+ * - Handles component prop spreading carefully to avoid conflicts
35
+ * - Maintains type safety through TypeScript interfaces
36
+ *
37
+ */
1
38
  import Parser from './Parser.svelte';
2
39
  import type { Renderers, Token, TokensList, Tokens } from './utils/markdown-parser.js';
3
40
  declare const Parser: import("svelte").Component<{
@@ -23,6 +23,29 @@
23
23
  @property {function} [parsed] - Callback function called with the parsed tokens
24
24
  -->
25
25
  <script lang="ts">
26
+ /**
27
+ * Component Evolution & Design Notes:
28
+ *
29
+ * 1. Core Purpose:
30
+ * - Serves as the main entry point for markdown rendering in Svelte
31
+ * - Handles both string input and pre-parsed tokens for flexibility
32
+ *
33
+ * 2. Key Design Decisions:
34
+ * - Uses a separate Parser component for actual rendering to maintain separation of concerns
35
+ * - Implements token cleanup via shrinkHtmlTokens to optimize HTML token handling
36
+ * - Maintains state synchronization using Svelte 5's $state and $effect
37
+ *
38
+ * 3. Performance Considerations:
39
+ * - Caches previous source to prevent unnecessary re-parsing
40
+ * - Uses key directive for proper component rerendering when source changes
41
+ * - Intentionally avoids reactive tokens to prevent double processing
42
+ *
43
+ * 4. Extensibility:
44
+ * - Supports custom renderers through composition pattern
45
+ * - Allows parser configuration via options prop
46
+ * - Provides parsed callback for external token access
47
+ */
48
+
26
49
  import {
27
50
  Lexer,
28
51
  defaultOptions,
@@ -30,19 +53,11 @@
30
53
  Slugger,
31
54
  type Token,
32
55
  type TokensList,
33
- type SvelteMarkdownOptions,
34
- type Renderers
56
+ type SvelteMarkdownOptions
35
57
  } from './utils/markdown-parser.js'
36
58
  import Parser from './Parser.svelte'
37
59
  import { shrinkHtmlTokens } from './utils/token-cleanup.js'
38
-
39
- interface Props {
40
- source: Token[] | string
41
- renderers?: Partial<Renderers>
42
- options?: SvelteMarkdownOptions
43
- isInline?: boolean
44
- parsed?: (tokens: Token[] | TokensList) => void // eslint-disable-line no-unused-vars
45
- }
60
+ import { type SvelteMarkdownProps } from './types.js'
46
61
 
47
62
  const {
48
63
  source = [],
@@ -51,34 +66,31 @@
51
66
  isInline = false,
52
67
  parsed = () => {},
53
68
  ...rest
54
- }: Props & {
69
+ }: SvelteMarkdownProps & {
55
70
  [key: string]: unknown
56
71
  } = $props()
57
- // @ts-expect-error - Intentionally not using $state for tokens
58
- let tokens: Token[] | undefined // eslint-disable-line svelte/valid-compile
59
- let previousSource = $state<string | Token[] | undefined>(undefined)
60
- let lexer: Lexer
61
72
 
62
- const slugger = source ? new Slugger() : undefined
63
73
  const combinedOptions = { ...defaultOptions, ...options }
74
+ const slugger = source ? new Slugger() : undefined
75
+ let lexer: Lexer
64
76
 
65
- $effect.pre(() => {
66
- if (source === previousSource) return
67
- previousSource = source
68
-
69
- if (Array.isArray(source)) {
70
- tokens = shrinkHtmlTokens(source) as Token[]
71
- } else {
77
+ const tokens = $derived.by(() => {
78
+ if (!lexer) {
72
79
  lexer = new Lexer(combinedOptions)
73
- tokens = shrinkHtmlTokens(
74
- isInline ? lexer.inlineTokens(source as string) : lexer.lex(source as string)
75
- )
76
80
  }
77
- })
81
+ if (Array.isArray(source)) {
82
+ return source as Token[]
83
+ }
84
+ return source
85
+ ? (shrinkHtmlTokens(
86
+ isInline ? lexer.inlineTokens(source as string) : lexer.lex(source as string)
87
+ ) as Token[])
88
+ : []
89
+ }) satisfies Token[] | TokensList | undefined
78
90
 
79
91
  $effect(() => {
80
92
  if (!tokens) return
81
- parsed($state.snapshot(tokens))
93
+ parsed(tokens)
82
94
  })
83
95
 
84
96
  const combinedRenderers = {
@@ -91,12 +103,10 @@
91
103
  }
92
104
  </script>
93
105
 
94
- {#key source}
95
- <Parser
96
- {tokens}
97
- {...rest}
98
- options={combinedOptions}
99
- slug={(val: string): string => (slugger ? slugger.slug(val) : '')}
100
- renderers={combinedRenderers}
101
- />
102
- {/key}
106
+ <Parser
107
+ {tokens}
108
+ {...rest}
109
+ options={combinedOptions}
110
+ slug={(val: string): string => (slugger ? slugger.slug(val) : '')}
111
+ renderers={combinedRenderers}
112
+ />
@@ -1,4 +1,7 @@
1
- import { type Token, type TokensList, type SvelteMarkdownOptions, type Renderers } from './utils/markdown-parser.js';
1
+ import { type SvelteMarkdownProps } from './types.js';
2
+ type $$ComponentProps = SvelteMarkdownProps & {
3
+ [key: string]: unknown;
4
+ };
2
5
  /**
3
6
  * A Svelte component that renders Markdown content into HTML using a customizable parser.
4
7
  * Supports both string input and pre-parsed markdown tokens, with configurable rendering
@@ -21,14 +24,6 @@ import { type Token, type TokensList, type SvelteMarkdownOptions, type Renderers
21
24
  * @property {boolean} [isInline=false] - Whether to parse the content as inline markdown
22
25
  * @property {function} [parsed] - Callback function called with the parsed tokens
23
26
  */
24
- declare const SvelteMarkdown: import("svelte").Component<{
25
- source: Token[] | string;
26
- renderers?: Partial<Renderers>;
27
- options?: SvelteMarkdownOptions;
28
- isInline?: boolean;
29
- parsed?: (tokens: Token[] | TokensList) => void;
30
- } & {
31
- [key: string]: unknown;
32
- }, {}, "">;
27
+ declare const SvelteMarkdown: import("svelte").Component<$$ComponentProps, {}, "">;
33
28
  type SvelteMarkdown = ReturnType<typeof SvelteMarkdown>;
34
29
  export default SvelteMarkdown;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SvelteMarkdownOptions, Token, TokensList } from './utils/markdown-parser.js';
2
2
  import SvelteMarkdown from './SvelteMarkdown.svelte';
3
+ import type { SvelteMarkdownProps } from './types.js';
3
4
  export default SvelteMarkdown;
4
- export type { SvelteMarkdownOptions, Token, TokensList };
5
+ export type { SvelteMarkdownOptions, SvelteMarkdownProps, Token, TokensList };
@@ -8,10 +8,13 @@
8
8
  }
9
9
 
10
10
  const { header, align, children }: Props = $props()
11
+
12
+ // Convert alignment to style object if alignment is specified
13
+ const style = $derived(align ? `text-align: ${align}` : undefined)
11
14
  </script>
12
15
 
13
16
  {#if header}
14
- <th {align}>{@render children?.()}</th>
17
+ <th {style}>{@render children?.()}</th>
15
18
  {:else}
16
- <td {align}>{@render children?.()}</td>
19
+ <td {style}>{@render children?.()}</td>
17
20
  {/if}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type definitions for the Svelte Markdown component.
3
+ *
4
+ * This module provides TypeScript type definitions for the core functionality
5
+ * of the Svelte Markdown parser and renderer. It defines the primary interface
6
+ * for component props and integrates with the marked library's token system.
7
+ *
8
+ * Typical usage example:
9
+ * ```typescript
10
+ * import type { SvelteMarkdownProps } from './types';
11
+ *
12
+ * const markdownProps: SvelteMarkdownProps = {
13
+ * source: "# Hello World",
14
+ * isInline: false
15
+ * };
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ import type { Token, TokensList } from 'marked';
21
+ import type { Renderers, SvelteMarkdownOptions } from './utils/markdown-parser.js';
22
+ export type SvelteMarkdownProps = {
23
+ source: Token[] | string;
24
+ renderers?: Partial<Renderers>;
25
+ options?: SvelteMarkdownOptions;
26
+ isInline?: boolean;
27
+ parsed?: (tokens: Token[] | TokensList) => void;
28
+ };
package/dist/types.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Type definitions for the Svelte Markdown component.
3
+ *
4
+ * This module provides TypeScript type definitions for the core functionality
5
+ * of the Svelte Markdown parser and renderer. It defines the primary interface
6
+ * for component props and integrates with the marked library's token system.
7
+ *
8
+ * Typical usage example:
9
+ * ```typescript
10
+ * import type { SvelteMarkdownProps } from './types';
11
+ *
12
+ * const markdownProps: SvelteMarkdownProps = {
13
+ * source: "# Hello World",
14
+ * isInline: false
15
+ * };
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ export {};
@@ -3,10 +3,25 @@ export { Lexer, type Token, type Tokens, type TokensList } from 'marked';
3
3
  import type { Component } from 'svelte';
4
4
  import { type HtmlRenderers } from '../renderers/html/index.js';
5
5
  /**
6
- * Type definition for markdown renderers
7
- * Maps each markdown element to its corresponding Svelte component
6
+ * Represents a Svelte component that can be used as a renderer.
7
+ * Allows for flexible component types while maintaining type safety.
8
+ *
9
+ * @typedef {Component<any, any, any> | undefined | null} RendererComponent
8
10
  */
9
11
  export type RendererComponent = Component<any, any, any> | undefined | null;
12
+ /**
13
+ * Comprehensive mapping of markdown elements to their renderer components.
14
+ * Structured in categories for better organization and maintainability.
15
+ *
16
+ * Categories:
17
+ * - HTML: Special renderer for HTML content
18
+ * - Block elements: Major structural elements
19
+ * - Table elements: Table-specific components
20
+ * - Inline elements: Text-level components
21
+ * - List variations: Specialized list item renderers
22
+ *
23
+ * @interface Renderers
24
+ */
10
25
  export type Renderers = {
11
26
  html: HtmlRenderers;
12
27
  heading: RendererComponent;
@@ -32,12 +47,48 @@ export type Renderers = {
32
47
  orderedlistitem: RendererComponent;
33
48
  unorderedlistitem: RendererComponent;
34
49
  };
50
+ /**
51
+ * Default renderer configuration mapping markdown elements to Svelte components.
52
+ * Provides out-of-the-box rendering capabilities while allowing for customization.
53
+ *
54
+ * Implementation notes:
55
+ * - All components are lazy-loaded for better performance
56
+ * - Null values indicate optional renderers
57
+ * - Components are type-checked against the Renderers interface
58
+ *
59
+ * @const {Renderers}
60
+ */
35
61
  export declare const defaultRenderers: Renderers;
62
+ /**
63
+ * Configuration options for SvelteMarkdown parser.
64
+ * Extends marked options with additional Svelte-specific configurations.
65
+ *
66
+ * @interface SvelteMarkdownOptions
67
+ *
68
+ * @property {string|null} baseUrl - Base URL for relative links
69
+ * @property {boolean} breaks - Enable line breaks in output
70
+ * @property {boolean} gfm - Enable GitHub Flavored Markdown
71
+ * @property {boolean} headerIds - Auto-generate header IDs
72
+ * @property {string} headerPrefix - Prefix for header IDs
73
+ * @property {Function|null} highlight - Syntax highlighting function
74
+ * @property {string} langPrefix - Prefix for code block language classes
75
+ * @property {boolean} mangle - Encode email addresses
76
+ * @property {boolean} pedantic - Conform to original markdown spec
77
+ * @property {Object|null} renderer - Custom renderer
78
+ * @property {boolean} sanitize - Sanitize HTML input
79
+ * @property {Function|null} sanitizer - Custom sanitizer function
80
+ * @property {boolean} silent - Suppress error output
81
+ * @property {boolean} smartLists - Use smarter list behavior
82
+ * @property {boolean} smartypants - Use smart punctuation
83
+ * @property {Object|null} tokenizer - Custom tokenizer
84
+ * @property {boolean} xhtml - Generate XHTML-compliant tags
85
+ */
36
86
  export type SvelteMarkdownOptions = {
37
87
  baseUrl: string | null;
38
88
  breaks: boolean;
39
89
  gfm: boolean;
40
90
  headerIds: boolean;
91
+ tables: boolean;
41
92
  headerPrefix: string;
42
93
  highlight: null;
43
94
  langPrefix: string;
@@ -52,4 +103,17 @@ export type SvelteMarkdownOptions = {
52
103
  tokenizer: null;
53
104
  xhtml: boolean;
54
105
  };
106
+ /**
107
+ * Default configuration options for the markdown parser.
108
+ * Provides sensible defaults while allowing for customization.
109
+ *
110
+ * Notable defaults:
111
+ * - GitHub Flavored Markdown enabled
112
+ * - Header IDs generated automatically
113
+ * - No syntax highlighting by default
114
+ * - HTML sanitization disabled
115
+ * - Standard markdown parsing rules
116
+ *
117
+ * @const {SvelteMarkdownOptions}
118
+ */
55
119
  export declare const defaultOptions: SvelteMarkdownOptions;
@@ -2,6 +2,17 @@ export { default as Slugger } from 'github-slugger';
2
2
  export { Lexer } from 'marked';
3
3
  import {} from '../renderers/html/index.js';
4
4
  import { Blockquote, Br, Code, Codespan, Del, Em, Heading, Hr, Html, Image, Link, List, ListItem, Paragraph, Strong, Table, TableBody, TableCell, TableHead, TableRow, Text } from '../renderers/index.js';
5
+ /**
6
+ * Default renderer configuration mapping markdown elements to Svelte components.
7
+ * Provides out-of-the-box rendering capabilities while allowing for customization.
8
+ *
9
+ * Implementation notes:
10
+ * - All components are lazy-loaded for better performance
11
+ * - Null values indicate optional renderers
12
+ * - Components are type-checked against the Renderers interface
13
+ *
14
+ * @const {Renderers}
15
+ */
5
16
  export const defaultRenderers = {
6
17
  heading: Heading,
7
18
  paragraph: Paragraph,
@@ -27,10 +38,24 @@ export const defaultRenderers = {
27
38
  code: Code,
28
39
  br: Br
29
40
  };
41
+ /**
42
+ * Default configuration options for the markdown parser.
43
+ * Provides sensible defaults while allowing for customization.
44
+ *
45
+ * Notable defaults:
46
+ * - GitHub Flavored Markdown enabled
47
+ * - Header IDs generated automatically
48
+ * - No syntax highlighting by default
49
+ * - HTML sanitization disabled
50
+ * - Standard markdown parsing rules
51
+ *
52
+ * @const {SvelteMarkdownOptions}
53
+ */
30
54
  export const defaultOptions = {
31
55
  baseUrl: null,
32
56
  breaks: false,
33
57
  gfm: true,
58
+ tables: true,
34
59
  headerIds: true,
35
60
  headerPrefix: '',
36
61
  highlight: null,
@@ -1,17 +1,132 @@
1
1
  import type { Token } from 'marked';
2
2
  /**
3
- * Determines if a string contains an HTML opening or closing tag
4
- * @param raw - The string to check for HTML tags
5
- * @returns Object containing the tag name and whether it's an opening tag, or null if no tag found
3
+ * Analyzes a string to determine if it contains an HTML tag and its characteristics.
4
+ *
5
+ * @param {string} raw - Raw string potentially containing an HTML tag
6
+ * @returns {Object|null} Returns null if no tag found, otherwise returns:
7
+ * {
8
+ * tag: string - The name of the HTML tag
9
+ * isOpening: bool - True if opening tag, false if closing
10
+ * }
11
+ *
12
+ * @example
13
+ * isHtmlOpenTag('<div class="test">') // Returns { tag: 'div', isOpening: true }
14
+ * isHtmlOpenTag('</span>') // Returns { tag: 'span', isOpening: false }
15
+ * isHtmlOpenTag('plain text') // Returns null
6
16
  */
7
17
  export declare const isHtmlOpenTag: (raw: string) => {
8
18
  tag: string;
9
19
  isOpening: boolean;
10
20
  } | null;
11
21
  /**
12
- * Main function to process and shrink HTML tokens
13
- * Breaks down complex HTML structures into manageable tokens
14
- * @param tokens - Array of tokens to process
15
- * @returns Processed array of tokens with nested structure
22
+ * Parses HTML attributes from a tag string into a structured object.
23
+ * Handles both single and double quoted attributes.
24
+ *
25
+ * @param {string} raw - Raw HTML tag string containing attributes
26
+ * @returns {Record<string, string>} Map of attribute names to their values
27
+ *
28
+ * @example
29
+ * extractAttributes('<div class="foo" id="bar">')
30
+ * // Returns { class: 'foo', id: 'bar' }
31
+ *
32
+ * @internal
33
+ */
34
+ export declare const extractAttributes: (raw: string) => Record<string, string>;
35
+ /**
36
+ * Converts an HTML string into a sequence of tokens using htmlparser2.
37
+ * Handles complex nested structures while maintaining proper order and relationships.
38
+ *
39
+ * Key features:
40
+ * - Preserves original HTML structure without automatic tag closing
41
+ * - Handles self-closing tags with proper XML syntax (e.g., <br/> instead of <br>)
42
+ * - Gracefully handles malformed HTML by preserving the original structure
43
+ * - Maintains attribute information in opening tags
44
+ * - Processes text content between tags
45
+ *
46
+ * @param {string} html - HTML string to be parsed
47
+ * @returns {Token[]} Array of tokens representing the HTML structure
48
+ *
49
+ * @example
50
+ * // Well-formed HTML
51
+ * parseHtmlBlock('<div>Hello <span>world</span></div>')
52
+ * // Returns [
53
+ * // { type: 'html', raw: '<div>', ... },
54
+ * // { type: 'text', raw: 'Hello ', ... },
55
+ * // { type: 'html', raw: '<span>', ... },
56
+ * // { type: 'text', raw: 'world', ... },
57
+ * // { type: 'html', raw: '</span>', ... },
58
+ * // { type: 'html', raw: '</div>', ... }
59
+ * // ]
60
+ *
61
+ * // Self-closing tags
62
+ * parseHtmlBlock('<div>Before<br/>After</div>')
63
+ * // Returns [
64
+ * // { type: 'html', raw: '<div>', ... },
65
+ * // { type: 'text', raw: 'Before', ... },
66
+ * // { type: 'html', raw: '<br/>', ... },
67
+ * // { type: 'text', raw: 'After', ... },
68
+ * // { type: 'html', raw: '</div>', ... }
69
+ * // ]
70
+ *
71
+ * // Malformed HTML
72
+ * parseHtmlBlock('<div>Unclosed')
73
+ * // Returns [
74
+ * // { type: 'html', raw: '<div>', ... },
75
+ * // { type: 'text', raw: 'Unclosed', ... }
76
+ * // ]
77
+ *
78
+ * @internal
79
+ */
80
+ export declare const parseHtmlBlock: (html: string) => Token[];
81
+ /**
82
+ * Determines if an HTML string contains multiple distinct tags.
83
+ * Used as a preprocessing step to optimize token processing.
84
+ *
85
+ * @param {string} html - HTML string to analyze
86
+ * @returns {boolean} True if multiple tags are present
87
+ *
88
+ * @internal
89
+ */
90
+ export declare const containsMultipleTags: (html: string) => boolean;
91
+ /**
92
+ * Primary entry point for HTML token processing. Transforms flat token arrays
93
+ * into properly nested structures while preserving HTML semantics.
94
+ *
95
+ * Key features:
96
+ * - Breaks down complex HTML structures into atomic tokens
97
+ * - Maintains attribute information
98
+ * - Preserves proper nesting relationships
99
+ * - Handles malformed HTML gracefully
100
+ *
101
+ * @param {Token[]} tokens - Array of tokens to process
102
+ * @returns {Token[]} Processed and properly nested token array
103
+ *
104
+ * @example
105
+ * const tokens = [
106
+ * { type: 'html', raw: '<div class="wrapper">' },
107
+ * { type: 'text', raw: 'content' },
108
+ * { type: 'html', raw: '</div>' }
109
+ * ];
110
+ * shrinkHtmlTokens(tokens);
111
+ * // Returns nested structure with proper token relationships
112
+ *
113
+ * @public
16
114
  */
17
115
  export declare const shrinkHtmlTokens: (tokens: Token[]) => Token[];
116
+ /**
117
+ * Core token processing logic that handles the complexities of HTML nesting.
118
+ * Uses a stack-based approach to match opening and closing tags while
119
+ * maintaining proper hierarchical relationships.
120
+ *
121
+ * Implementation details:
122
+ * - Maintains a stack of opening tags
123
+ * - Processes nested tokens recursively
124
+ * - Preserves HTML attributes
125
+ * - Handles malformed HTML gracefully
126
+ *
127
+ * @param {Token[]} tokens - Tokens to be processed
128
+ * @returns {Token[]} Processed tokens with proper nesting structure
129
+ *
130
+ * @internal
131
+ */
132
+ export declare const processHtmlTokens: (tokens: Token[]) => Token[];
@@ -1,15 +1,31 @@
1
1
  import { Parser } from 'htmlparser2';
2
2
  /**
3
- * Regular expression pattern to match HTML tags
4
- * Matches both opening and closing tags with optional attributes
5
- * Example matches: <div>, </div>, <img src="...">, <input type="text"/>
3
+ * Matches HTML tags with comprehensive coverage of edge cases.
4
+ * Pattern breakdown:
5
+ * - <\/? : Matches opening < and optional /
6
+ * - [a-zA-Z] : Tag must start with letter
7
+ * - [a-zA-Z0-9-] : Subsequent chars can be letters, numbers, or hyphens
8
+ * - (?:\s+[^>]*)?: Optional attributes
9
+ * - > : Closing bracket
10
+ *
11
+ * @const {RegExp}
6
12
  */
7
13
  const HTML_TAG_PATTERN = /<\/?([a-zA-Z][a-zA-Z0-9-]{0,})(?:\s+[^>]*)?>/;
8
14
  const htmlTagRegex = new RegExp(HTML_TAG_PATTERN);
9
15
  /**
10
- * Determines if a string contains an HTML opening or closing tag
11
- * @param raw - The string to check for HTML tags
12
- * @returns Object containing the tag name and whether it's an opening tag, or null if no tag found
16
+ * Analyzes a string to determine if it contains an HTML tag and its characteristics.
17
+ *
18
+ * @param {string} raw - Raw string potentially containing an HTML tag
19
+ * @returns {Object|null} Returns null if no tag found, otherwise returns:
20
+ * {
21
+ * tag: string - The name of the HTML tag
22
+ * isOpening: bool - True if opening tag, false if closing
23
+ * }
24
+ *
25
+ * @example
26
+ * isHtmlOpenTag('<div class="test">') // Returns { tag: 'div', isOpening: true }
27
+ * isHtmlOpenTag('</span>') // Returns { tag: 'span', isOpening: false }
28
+ * isHtmlOpenTag('plain text') // Returns null
13
29
  */
14
30
  export const isHtmlOpenTag = (raw) => {
15
31
  // First check if the string contains any HTML tags at all (faster than full regex match)
@@ -22,36 +38,89 @@ export const isHtmlOpenTag = (raw) => {
22
38
  return { tag: match[1], isOpening: !raw.startsWith('</') };
23
39
  };
24
40
  /**
25
- * Extracts HTML attributes from a tag string
26
- * @param raw - The raw HTML tag string (e.g., '<div class="example" id="test">')
27
- * @returns An object containing key-value pairs of attributes
41
+ * Parses HTML attributes from a tag string into a structured object.
42
+ * Handles both single and double quoted attributes.
43
+ *
44
+ * @param {string} raw - Raw HTML tag string containing attributes
45
+ * @returns {Record<string, string>} Map of attribute names to their values
46
+ *
47
+ * @example
48
+ * extractAttributes('<div class="foo" id="bar">')
49
+ * // Returns { class: 'foo', id: 'bar' }
50
+ *
51
+ * @internal
28
52
  */
29
- const extractAttributes = (raw) => {
53
+ export const extractAttributes = (raw) => {
30
54
  const attributes = {};
31
- // Match pattern: attribute="value" or attribute='value'
32
- const attributeRegex = /(\w+)=["']([^"']*?)["']/g;
55
+ // First pass: handle regular and unclosed quoted attributes
56
+ const quotedRegex = /([a-zA-Z][\w-]*?)=["']([^"']*?)(?:["']|$)/g;
33
57
  let match;
34
- // Continue finding matches until we've processed all attributes
35
- while ((match = attributeRegex.exec(raw)) !== null) {
58
+ while ((match = quotedRegex.exec(raw)) !== null) {
36
59
  const [, key, value] = match;
37
60
  attributes[key] = value.trim();
38
61
  }
62
+ // Second pass: handle boolean attributes
63
+ const booleanRegex = /(?:^|\s)([a-zA-Z][\w-]*?)(?=[\s>]|$)/g;
64
+ while ((match = booleanRegex.exec(raw)) !== null) {
65
+ const [, key] = match;
66
+ if (key && !attributes[key]) {
67
+ attributes[key] = '';
68
+ }
69
+ }
39
70
  return attributes;
40
71
  };
41
72
  /**
42
- * Parses an HTML string into an array of tokens
43
- * Uses htmlparser2 to properly handle nested tags and text content
44
- * @param html - The HTML string to parse
45
- * @returns Array of tokens representing the HTML structure
73
+ * Converts an HTML string into a sequence of tokens using htmlparser2.
74
+ * Handles complex nested structures while maintaining proper order and relationships.
75
+ *
76
+ * Key features:
77
+ * - Preserves original HTML structure without automatic tag closing
78
+ * - Handles self-closing tags with proper XML syntax (e.g., <br/> instead of <br>)
79
+ * - Gracefully handles malformed HTML by preserving the original structure
80
+ * - Maintains attribute information in opening tags
81
+ * - Processes text content between tags
82
+ *
83
+ * @param {string} html - HTML string to be parsed
84
+ * @returns {Token[]} Array of tokens representing the HTML structure
85
+ *
86
+ * @example
87
+ * // Well-formed HTML
88
+ * parseHtmlBlock('<div>Hello <span>world</span></div>')
89
+ * // Returns [
90
+ * // { type: 'html', raw: '<div>', ... },
91
+ * // { type: 'text', raw: 'Hello ', ... },
92
+ * // { type: 'html', raw: '<span>', ... },
93
+ * // { type: 'text', raw: 'world', ... },
94
+ * // { type: 'html', raw: '</span>', ... },
95
+ * // { type: 'html', raw: '</div>', ... }
96
+ * // ]
97
+ *
98
+ * // Self-closing tags
99
+ * parseHtmlBlock('<div>Before<br/>After</div>')
100
+ * // Returns [
101
+ * // { type: 'html', raw: '<div>', ... },
102
+ * // { type: 'text', raw: 'Before', ... },
103
+ * // { type: 'html', raw: '<br/>', ... },
104
+ * // { type: 'text', raw: 'After', ... },
105
+ * // { type: 'html', raw: '</div>', ... }
106
+ * // ]
107
+ *
108
+ * // Malformed HTML
109
+ * parseHtmlBlock('<div>Unclosed')
110
+ * // Returns [
111
+ * // { type: 'html', raw: '<div>', ... },
112
+ * // { type: 'text', raw: 'Unclosed', ... }
113
+ * // ]
114
+ *
115
+ * @internal
46
116
  */
47
- const parseHtmlBlock = (html) => {
117
+ export const parseHtmlBlock = (html) => {
48
118
  const tokens = [];
49
- // Buffer for accumulating text content between tags
50
119
  let currentText = '';
120
+ const selfClosingTags = /^(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)$/i;
121
+ const openTags = [];
51
122
  const parser = new Parser({
52
- // Called when an opening tag is encountered (<div>, <span>, etc.)
53
123
  onopentag: (name, attributes) => {
54
- // If we have accumulated any text, create a text token first
55
124
  if (currentText.trim()) {
56
125
  tokens.push({
57
126
  type: 'text',
@@ -60,23 +129,32 @@ const parseHtmlBlock = (html) => {
60
129
  });
61
130
  currentText = '';
62
131
  }
63
- // Create a token for the opening tag with its attributes
64
- tokens.push({
65
- type: 'html',
66
- raw: `<${name}${Object.entries(attributes)
67
- .map(([key, value]) => ` ${key}="${value}"`)
68
- .join('')}>`,
69
- tag: name,
70
- attributes
71
- });
132
+ openTags.push(name);
133
+ if (selfClosingTags.test(name)) {
134
+ tokens.push({
135
+ type: 'html',
136
+ raw: `<${name}${Object.entries(attributes)
137
+ .map(([key, value]) => ` ${key}="${value}"`)
138
+ .join('')}/>`,
139
+ tag: name,
140
+ attributes
141
+ });
142
+ }
143
+ else {
144
+ tokens.push({
145
+ type: 'html',
146
+ raw: `<${name}${Object.entries(attributes)
147
+ .map(([key, value]) => ` ${key}="${value}"`)
148
+ .join('')}>`,
149
+ tag: name,
150
+ attributes
151
+ });
152
+ }
72
153
  },
73
- // Called for text content between tags
74
154
  ontext: (text) => {
75
155
  currentText += text;
76
156
  },
77
- // Called when a closing tag is encountered (</div>, </span>, etc.)
78
157
  onclosetag: (name) => {
79
- // Push any accumulated text before the closing tag
80
158
  if (currentText.trim()) {
81
159
  tokens.push({
82
160
  type: 'text',
@@ -85,41 +163,95 @@ const parseHtmlBlock = (html) => {
85
163
  });
86
164
  currentText = '';
87
165
  }
88
- // Create a token for the closing tag
89
- tokens.push({
90
- type: 'html',
91
- raw: `</${name}>`,
92
- tag: name
93
- });
166
+ // Only add closing tag if we found its opening tag
167
+ // and it's not a self-closing tag
168
+ if (openTags.includes(name) && !selfClosingTags.test(name)) {
169
+ if (html.includes(`</${name}>`)) {
170
+ tokens.push({
171
+ type: 'html',
172
+ raw: `</${name}>`,
173
+ tag: name
174
+ });
175
+ }
176
+ openTags.splice(openTags.indexOf(name), 1);
177
+ }
94
178
  }
179
+ }, {
180
+ xmlMode: true,
181
+ // Add this to prevent automatic tag closing
182
+ recognizeSelfClosing: true
95
183
  });
96
- // Process the HTML string
97
184
  parser.write(html);
98
185
  parser.end();
186
+ if (currentText.trim()) {
187
+ tokens.push({
188
+ type: 'text',
189
+ raw: currentText,
190
+ text: currentText
191
+ });
192
+ }
99
193
  return tokens;
100
194
  };
101
195
  /**
102
- * Checks if an HTML string contains multiple tags
103
- * Used to determine if further parsing is needed
104
- * @param html - The HTML string to check
105
- * @returns boolean indicating if multiple tags are present
196
+ * Determines if an HTML string contains multiple distinct tags.
197
+ * Used as a preprocessing step to optimize token processing.
198
+ *
199
+ * @param {string} html - HTML string to analyze
200
+ * @returns {boolean} True if multiple tags are present
201
+ *
202
+ * @internal
106
203
  */
107
- const containsMultipleTags = (html) => {
204
+ export const containsMultipleTags = (html) => {
108
205
  // Count the number of opening tags (excluding self-closing)
109
206
  const openingTags = html.match(/<[a-zA-Z][^>]*>/g) || [];
110
207
  const closingTags = html.match(/<\/[a-zA-Z][^>]*>/g) || [];
111
208
  return openingTags.length > 1 || closingTags.length > 1;
112
209
  };
113
210
  /**
114
- * Main function to process and shrink HTML tokens
115
- * Breaks down complex HTML structures into manageable tokens
116
- * @param tokens - Array of tokens to process
117
- * @returns Processed array of tokens with nested structure
211
+ * Primary entry point for HTML token processing. Transforms flat token arrays
212
+ * into properly nested structures while preserving HTML semantics.
213
+ *
214
+ * Key features:
215
+ * - Breaks down complex HTML structures into atomic tokens
216
+ * - Maintains attribute information
217
+ * - Preserves proper nesting relationships
218
+ * - Handles malformed HTML gracefully
219
+ *
220
+ * @param {Token[]} tokens - Array of tokens to process
221
+ * @returns {Token[]} Processed and properly nested token array
222
+ *
223
+ * @example
224
+ * const tokens = [
225
+ * { type: 'html', raw: '<div class="wrapper">' },
226
+ * { type: 'text', raw: 'content' },
227
+ * { type: 'html', raw: '</div>' }
228
+ * ];
229
+ * shrinkHtmlTokens(tokens);
230
+ * // Returns nested structure with proper token relationships
231
+ *
232
+ * @public
118
233
  */
119
234
  export const shrinkHtmlTokens = (tokens) => {
120
235
  const result = [];
121
236
  for (const token of tokens) {
122
- if (token.type === 'html' && containsMultipleTags(token.raw)) {
237
+ if (token.type === 'table') {
238
+ // Process header cells
239
+ if (token.header) {
240
+ token.header = token.header.map((cell) => ({
241
+ ...cell,
242
+ tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
243
+ }));
244
+ }
245
+ // Process row cells
246
+ if (token.rows) {
247
+ token.rows = token.rows.map((row) => row.map((cell) => ({
248
+ ...cell,
249
+ tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
250
+ })));
251
+ }
252
+ result.push(token);
253
+ }
254
+ else if (token.type === 'html' && containsMultipleTags(token.raw)) {
123
255
  // Parse HTML with multiple tags into separate tokens
124
256
  result.push(...parseHtmlBlock(token.raw));
125
257
  }
@@ -131,26 +263,22 @@ export const shrinkHtmlTokens = (tokens) => {
131
263
  return processHtmlTokens(result);
132
264
  };
133
265
  /**
134
- * Processes HTML tokens to create a nested structure
135
- * Handles matching opening and closing tags, maintains proper nesting
136
- * and preserves attributes
266
+ * Core token processing logic that handles the complexities of HTML nesting.
267
+ * Uses a stack-based approach to match opening and closing tags while
268
+ * maintaining proper hierarchical relationships.
137
269
  *
138
- * @param tokens - Array of tokens to process
139
- * @returns Processed array of tokens with proper nesting structure
270
+ * Implementation details:
271
+ * - Maintains a stack of opening tags
272
+ * - Processes nested tokens recursively
273
+ * - Preserves HTML attributes
274
+ * - Handles malformed HTML gracefully
140
275
  *
141
- * @example
142
- * Input tokens: [
143
- * { type: 'html', raw: '<div>' },
144
- * { type: 'text', raw: 'Hello' },
145
- * { type: 'html', raw: '</div>' }
146
- * ]
147
- * Output: [
148
- * { type: 'html', tag: 'div', tokens: [
149
- * { type: 'text', raw: 'Hello' }
150
- * ]}
151
- * ]
276
+ * @param {Token[]} tokens - Tokens to be processed
277
+ * @returns {Token[]} Processed tokens with proper nesting structure
278
+ *
279
+ * @internal
152
280
  */
153
- const processHtmlTokens = (tokens) => {
281
+ export const processHtmlTokens = (tokens) => {
154
282
  const result = [];
155
283
  // Stack to keep track of opening tags and their positions
156
284
  const stack = [];
package/package.json CHANGED
@@ -1,13 +1,30 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-markdown",
3
+ "version": "0.7.5",
3
4
  "description": "A powerful, customizable markdown renderer for Svelte with TypeScript support",
4
- "version": "0.7.3",
5
+ "type": "module",
6
+ "svelte": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "svelte": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "!dist/**/*.test.*",
17
+ "!dist/**/*.spec.*"
18
+ ],
19
+ "sideEffects": [
20
+ "**/*.css"
21
+ ],
5
22
  "scripts": {
6
23
  "dev": "vite dev",
7
24
  "build": "vite build && npm run package",
8
- "preview": "vite preview",
9
25
  "package": "svelte-kit sync && svelte-package && publint",
10
26
  "prepublishOnly": "npm run package",
27
+ "preview": "vite preview",
11
28
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
29
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
30
  "test": "vitest run --coverage",
@@ -17,60 +34,14 @@
17
34
  "lint:fix": "npm run format && eslint . --fix",
18
35
  "format": "prettier --write ."
19
36
  },
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/humanspeak/svelte-markdown.git"
23
- },
24
- "author": "Humanspeak, Inc.",
25
- "license": "MIT",
26
- "bugs": {
27
- "url": "https://github.com/humanspeak/svelte-markdown/issues"
28
- },
29
- "tags": [
30
- "svelte",
31
- "markdown"
32
- ],
33
- "keywords": [
34
- "svelte",
35
- "markdown",
36
- "renderer",
37
- "parser",
38
- "marked",
39
- "component",
40
- "sveltekit",
41
- "svelte5",
42
- "md",
43
- "documentation",
44
- "html",
45
- "converter",
46
- "formatting"
47
- ],
48
- "homepage": "https://markdown.svelte.page",
49
- "files": [
50
- "dist",
51
- "!dist/**/*.test.*",
52
- "!dist/**/*.spec.*"
53
- ],
54
- "sideEffects": [
55
- "**/*.css"
56
- ],
57
- "svelte": "./dist/index.js",
58
- "types": "./dist/index.d.ts",
59
- "type": "module",
60
- "exports": {
61
- ".": {
62
- "types": "./dist/index.d.ts",
63
- "svelte": "./dist/index.js"
64
- }
65
- },
66
- "peerDependencies": {
67
- "svelte": "^5.0.0"
68
- },
69
37
  "dependencies": {
70
38
  "github-slugger": "^2.0.0",
71
39
  "htmlparser2": "^10.0.0",
72
40
  "marked": "^15.0.5"
73
41
  },
42
+ "peerDependencies": {
43
+ "svelte": "^5.0.0"
44
+ },
74
45
  "devDependencies": {
75
46
  "@eslint/eslintrc": "^3.2.0",
76
47
  "@eslint/js": "^9.17.0",
@@ -84,7 +55,7 @@
84
55
  "@types/node": "^22.10.5",
85
56
  "@typescript-eslint/eslint-plugin": "^8.19.0",
86
57
  "@typescript-eslint/parser": "^8.19.0",
87
- "@vitest/coverage-v8": "^2.1.8",
58
+ "@vitest/coverage-v8": "^3.0.0-beta.3",
88
59
  "eslint": "^9.17.0",
89
60
  "eslint-config-prettier": "^9.1.0",
90
61
  "eslint-plugin-svelte": "^2.46.1",
@@ -99,21 +70,50 @@
99
70
  "svelte-check": "^4.1.1",
100
71
  "typescript": "^5.7.2",
101
72
  "vite": "^6.0.7",
102
- "vitest": "^2.1.8"
73
+ "vitest": "^3.0.0-beta.3"
103
74
  },
104
- "overrides": {
105
- "@sveltejs/kit": {
106
- "cookie": "^0.7.0"
107
- }
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/humanspeak/svelte-markdown.git"
108
78
  },
109
- "volta": {
110
- "node": "22.12.0"
79
+ "homepage": "https://markdown.svelte.page",
80
+ "bugs": {
81
+ "url": "https://github.com/humanspeak/svelte-markdown/issues"
111
82
  },
83
+ "keywords": [
84
+ "svelte",
85
+ "markdown",
86
+ "renderer",
87
+ "parser",
88
+ "marked",
89
+ "component",
90
+ "sveltekit",
91
+ "svelte5",
92
+ "md",
93
+ "documentation",
94
+ "html",
95
+ "converter",
96
+ "formatting"
97
+ ],
98
+ "tags": [
99
+ "svelte",
100
+ "markdown"
101
+ ],
102
+ "author": "Humanspeak, Inc.",
103
+ "license": "MIT",
112
104
  "funding": {
113
105
  "type": "github",
114
106
  "url": "https://github.com/sponsors/humanspeak"
115
107
  },
116
108
  "publishConfig": {
117
109
  "access": "public"
110
+ },
111
+ "overrides": {
112
+ "@sveltejs/kit": {
113
+ "cookie": "^0.7.0"
114
+ }
115
+ },
116
+ "volta": {
117
+ "node": "22.12.0"
118
118
  }
119
119
  }