@neeleshyadav/react-native-html-renderer 1.1.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 (155) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +407 -0
  3. package/lib/module/HtmlRenderer.js +183 -0
  4. package/lib/module/HtmlRenderer.js.map +1 -0
  5. package/lib/module/context/index.js +32 -0
  6. package/lib/module/context/index.js.map +1 -0
  7. package/lib/module/hooks/index.js +6 -0
  8. package/lib/module/hooks/index.js.map +1 -0
  9. package/lib/module/hooks/useContentWidth.js +12 -0
  10. package/lib/module/hooks/useContentWidth.js.map +1 -0
  11. package/lib/module/hooks/useHtmlParser.js +16 -0
  12. package/lib/module/hooks/useHtmlParser.js.map +1 -0
  13. package/lib/module/hooks/useTagStyle.js +26 -0
  14. package/lib/module/hooks/useTagStyle.js.map +1 -0
  15. package/lib/module/index.js +23 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/module/parser/index.js +62 -0
  19. package/lib/module/parser/index.js.map +1 -0
  20. package/lib/module/renderer/ErrorBoundary.js +66 -0
  21. package/lib/module/renderer/ErrorBoundary.js.map +1 -0
  22. package/lib/module/renderer/NodeRenderer.js +279 -0
  23. package/lib/module/renderer/NodeRenderer.js.map +1 -0
  24. package/lib/module/renderer/index.js +5 -0
  25. package/lib/module/renderer/index.js.map +1 -0
  26. package/lib/module/renderer/tags/BlockTags.js +28 -0
  27. package/lib/module/renderer/tags/BlockTags.js.map +1 -0
  28. package/lib/module/renderer/tags/FormTags.js +129 -0
  29. package/lib/module/renderer/tags/FormTags.js.map +1 -0
  30. package/lib/module/renderer/tags/ImageTag.js +163 -0
  31. package/lib/module/renderer/tags/ImageTag.js.map +1 -0
  32. package/lib/module/renderer/tags/LinkTag.js +50 -0
  33. package/lib/module/renderer/tags/LinkTag.js.map +1 -0
  34. package/lib/module/renderer/tags/ListTags.js +96 -0
  35. package/lib/module/renderer/tags/ListTags.js.map +1 -0
  36. package/lib/module/renderer/tags/MediaTags.js +69 -0
  37. package/lib/module/renderer/tags/MediaTags.js.map +1 -0
  38. package/lib/module/renderer/tags/TableTags.js +48 -0
  39. package/lib/module/renderer/tags/TableTags.js.map +1 -0
  40. package/lib/module/renderer/tags/TextTags.js +87 -0
  41. package/lib/module/renderer/tags/TextTags.js.map +1 -0
  42. package/lib/module/renderer/tags/index.js +11 -0
  43. package/lib/module/renderer/tags/index.js.map +1 -0
  44. package/lib/module/styles/cssToRn.js +34 -0
  45. package/lib/module/styles/cssToRn.js.map +1 -0
  46. package/lib/module/styles/darkModeStyles.js +81 -0
  47. package/lib/module/styles/darkModeStyles.js.map +1 -0
  48. package/lib/module/styles/defaultStyles.js +218 -0
  49. package/lib/module/styles/defaultStyles.js.map +1 -0
  50. package/lib/module/styles/index.js +7 -0
  51. package/lib/module/styles/index.js.map +1 -0
  52. package/lib/module/styles/mergeStyles.js +47 -0
  53. package/lib/module/styles/mergeStyles.js.map +1 -0
  54. package/lib/module/types/index.js +4 -0
  55. package/lib/module/types/index.js.map +1 -0
  56. package/lib/module/utils/accessibility.js +108 -0
  57. package/lib/module/utils/accessibility.js.map +1 -0
  58. package/lib/module/utils/cache.js +69 -0
  59. package/lib/module/utils/cache.js.map +1 -0
  60. package/lib/module/utils/index.js +95 -0
  61. package/lib/module/utils/index.js.map +1 -0
  62. package/lib/module/utils/sanitize.js +102 -0
  63. package/lib/module/utils/sanitize.js.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/HtmlRenderer.d.ts +15 -0
  66. package/lib/typescript/src/HtmlRenderer.d.ts.map +1 -0
  67. package/lib/typescript/src/context/index.d.ts +5 -0
  68. package/lib/typescript/src/context/index.d.ts.map +1 -0
  69. package/lib/typescript/src/hooks/index.d.ts +4 -0
  70. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  71. package/lib/typescript/src/hooks/useContentWidth.d.ts +6 -0
  72. package/lib/typescript/src/hooks/useContentWidth.d.ts.map +1 -0
  73. package/lib/typescript/src/hooks/useHtmlParser.d.ts +11 -0
  74. package/lib/typescript/src/hooks/useHtmlParser.d.ts.map +1 -0
  75. package/lib/typescript/src/hooks/useTagStyle.d.ts +11 -0
  76. package/lib/typescript/src/hooks/useTagStyle.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +9 -0
  78. package/lib/typescript/src/index.d.ts.map +1 -0
  79. package/lib/typescript/src/parser/index.d.ts +10 -0
  80. package/lib/typescript/src/parser/index.d.ts.map +1 -0
  81. package/lib/typescript/src/renderer/ErrorBoundary.d.ts +22 -0
  82. package/lib/typescript/src/renderer/ErrorBoundary.d.ts.map +1 -0
  83. package/lib/typescript/src/renderer/NodeRenderer.d.ts +7 -0
  84. package/lib/typescript/src/renderer/NodeRenderer.d.ts.map +1 -0
  85. package/lib/typescript/src/renderer/index.d.ts +3 -0
  86. package/lib/typescript/src/renderer/index.d.ts.map +1 -0
  87. package/lib/typescript/src/renderer/tags/BlockTags.d.ts +18 -0
  88. package/lib/typescript/src/renderer/tags/BlockTags.d.ts.map +1 -0
  89. package/lib/typescript/src/renderer/tags/FormTags.d.ts +16 -0
  90. package/lib/typescript/src/renderer/tags/FormTags.d.ts.map +1 -0
  91. package/lib/typescript/src/renderer/tags/ImageTag.d.ts +18 -0
  92. package/lib/typescript/src/renderer/tags/ImageTag.d.ts.map +1 -0
  93. package/lib/typescript/src/renderer/tags/LinkTag.d.ts +19 -0
  94. package/lib/typescript/src/renderer/tags/LinkTag.d.ts.map +1 -0
  95. package/lib/typescript/src/renderer/tags/ListTags.d.ts +15 -0
  96. package/lib/typescript/src/renderer/tags/ListTags.d.ts.map +1 -0
  97. package/lib/typescript/src/renderer/tags/MediaTags.d.ts +14 -0
  98. package/lib/typescript/src/renderer/tags/MediaTags.d.ts.map +1 -0
  99. package/lib/typescript/src/renderer/tags/TableTags.d.ts +15 -0
  100. package/lib/typescript/src/renderer/tags/TableTags.d.ts.map +1 -0
  101. package/lib/typescript/src/renderer/tags/TextTags.d.ts +22 -0
  102. package/lib/typescript/src/renderer/tags/TextTags.d.ts.map +1 -0
  103. package/lib/typescript/src/renderer/tags/index.d.ts +9 -0
  104. package/lib/typescript/src/renderer/tags/index.d.ts.map +1 -0
  105. package/lib/typescript/src/styles/cssToRn.d.ts +11 -0
  106. package/lib/typescript/src/styles/cssToRn.d.ts.map +1 -0
  107. package/lib/typescript/src/styles/darkModeStyles.d.ts +7 -0
  108. package/lib/typescript/src/styles/darkModeStyles.d.ts.map +1 -0
  109. package/lib/typescript/src/styles/defaultStyles.d.ts +8 -0
  110. package/lib/typescript/src/styles/defaultStyles.d.ts.map +1 -0
  111. package/lib/typescript/src/styles/index.d.ts +5 -0
  112. package/lib/typescript/src/styles/index.d.ts.map +1 -0
  113. package/lib/typescript/src/styles/mergeStyles.d.ts +10 -0
  114. package/lib/typescript/src/styles/mergeStyles.d.ts.map +1 -0
  115. package/lib/typescript/src/types/index.d.ts +158 -0
  116. package/lib/typescript/src/types/index.d.ts.map +1 -0
  117. package/lib/typescript/src/utils/accessibility.d.ts +32 -0
  118. package/lib/typescript/src/utils/accessibility.d.ts.map +1 -0
  119. package/lib/typescript/src/utils/cache.d.ts +24 -0
  120. package/lib/typescript/src/utils/cache.d.ts.map +1 -0
  121. package/lib/typescript/src/utils/index.d.ts +33 -0
  122. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/sanitize.d.ts +11 -0
  124. package/lib/typescript/src/utils/sanitize.d.ts.map +1 -0
  125. package/package.json +171 -0
  126. package/src/HtmlRenderer.tsx +216 -0
  127. package/src/context/index.tsx +30 -0
  128. package/src/hooks/index.ts +3 -0
  129. package/src/hooks/useContentWidth.ts +9 -0
  130. package/src/hooks/useHtmlParser.ts +18 -0
  131. package/src/hooks/useTagStyle.ts +23 -0
  132. package/src/index.tsx +39 -0
  133. package/src/parser/index.ts +80 -0
  134. package/src/renderer/ErrorBoundary.tsx +80 -0
  135. package/src/renderer/NodeRenderer.tsx +345 -0
  136. package/src/renderer/index.tsx +2 -0
  137. package/src/renderer/tags/BlockTags.tsx +49 -0
  138. package/src/renderer/tags/FormTags.tsx +169 -0
  139. package/src/renderer/tags/ImageTag.tsx +215 -0
  140. package/src/renderer/tags/LinkTag.tsx +76 -0
  141. package/src/renderer/tags/ListTags.tsx +148 -0
  142. package/src/renderer/tags/MediaTags.tsx +81 -0
  143. package/src/renderer/tags/TableTags.tsx +94 -0
  144. package/src/renderer/tags/TextTags.tsx +139 -0
  145. package/src/renderer/tags/index.ts +8 -0
  146. package/src/styles/cssToRn.ts +45 -0
  147. package/src/styles/darkModeStyles.ts +80 -0
  148. package/src/styles/defaultStyles.ts +176 -0
  149. package/src/styles/index.ts +4 -0
  150. package/src/styles/mergeStyles.ts +59 -0
  151. package/src/types/index.ts +229 -0
  152. package/src/utils/accessibility.ts +132 -0
  153. package/src/utils/cache.ts +83 -0
  154. package/src/utils/index.ts +151 -0
  155. package/src/utils/sanitize.ts +149 -0
@@ -0,0 +1,229 @@
1
+ import type { ReactNode } from 'react';
2
+ import type {
3
+ TextStyle,
4
+ ViewStyle,
5
+ ImageStyle,
6
+ TextProps,
7
+ ViewProps,
8
+ ColorSchemeName,
9
+ } from 'react-native';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // DOM Nodes
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** A parsed DOM node — either an element or a text node. */
16
+ export type DOMNode = DOMElement | DOMText;
17
+
18
+ /** An HTML element node with tag, attributes, and children. */
19
+ export interface DOMElement {
20
+ type: 'element';
21
+ tag: string;
22
+ attributes: Record<string, string>;
23
+ children: DOMNode[];
24
+ }
25
+
26
+ /** A text node containing raw string data. */
27
+ export interface DOMText {
28
+ type: 'text';
29
+ data: string;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Styles
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /** Union of all React Native style types. */
37
+ export type RNStyle = ViewStyle | TextStyle | ImageStyle;
38
+
39
+ /** Per-tag style overrides keyed by HTML tag name. */
40
+ export type TagsStyles = Record<string, RNStyle>;
41
+
42
+ /** Per-class style overrides keyed by HTML class name. */
43
+ export type ClassesStyles = Record<string, RNStyle>;
44
+
45
+ /** Per-id style overrides keyed by HTML element id. */
46
+ export type IdsStyles = Record<string, RNStyle>;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Custom Renderers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /** Props passed to a custom renderer function. */
53
+ export interface CustomRendererProps {
54
+ /** The DOM element node being rendered. */
55
+ node: DOMElement;
56
+ /** Pre-rendered children as React nodes. */
57
+ children: ReactNode[];
58
+ /** Merged style for this element. */
59
+ style: RNStyle;
60
+ /** Attributes from the HTML element. */
61
+ attributes: Record<string, string>;
62
+ /** Extra props passed via renderersProps[tag]. */
63
+ passProps: Record<string, unknown>;
64
+ /** Render helper — call with child DOMNodes to render them. */
65
+ renderChildren: (nodes: DOMNode[]) => ReactNode[];
66
+ /** Current content width from context. */
67
+ contentWidth: number;
68
+ }
69
+
70
+ /** A custom renderer receives props and returns a ReactNode. */
71
+ export type CustomRenderer = (props: CustomRendererProps) => ReactNode;
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // List prefix renderers
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export interface ListPrefixRendererProps {
78
+ index: number;
79
+ nestLevel: number;
80
+ }
81
+
82
+ export interface ListsPrefixesRenderers {
83
+ ul?: (props: ListPrefixRendererProps) => ReactNode;
84
+ ol?: (props: ListPrefixRendererProps) => ReactNode;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // HtmlRenderer Props
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /** Props for the main `<HtmlRenderer />` component. */
92
+ export interface HtmlRendererProps {
93
+ /** Raw HTML string to render. */
94
+ html: string;
95
+
96
+ /** Available width for content layout and image scaling. */
97
+ contentWidth: number;
98
+
99
+ /** Base style applied to the root container. */
100
+ baseStyle?: ViewStyle;
101
+
102
+ /** Per-tag style overrides. */
103
+ tagsStyles?: TagsStyles;
104
+
105
+ /** Styles applied by HTML class name. */
106
+ classesStyles?: ClassesStyles;
107
+
108
+ /** Styles applied by HTML element id. */
109
+ idsStyles?: IdsStyles;
110
+
111
+ /** Override rendering for specific tags. */
112
+ customRenderers?: Record<string, CustomRenderer>;
113
+
114
+ /** Called when a link (`<a>`) is pressed. */
115
+ onLinkPress?: (href: string, attributes: Record<string, string>) => void;
116
+
117
+ /** Called when an image is pressed. */
118
+ onImagePress?: (src: string, attributes: Record<string, string>) => void;
119
+
120
+ /** Called when an error occurs during parsing or rendering. */
121
+ onError?: (error: Error) => void;
122
+
123
+ /** Custom fallback UI to show when an error occurs. If omitted, a default error message is shown. Set to `null` to render nothing on error. */
124
+ fallback?: ReactNode;
125
+
126
+ /** Tags to completely ignore (including their children). */
127
+ ignoredTags?: string[];
128
+
129
+ /** CSS property names to ignore during style conversion. */
130
+ ignoredStyles?: string[];
131
+
132
+ /** Whitelist of CSS property names to allow (if set, only these are kept). */
133
+ allowedStyles?: string[];
134
+
135
+ /** Default props passed to every `<Text>` component. */
136
+ defaultTextProps?: TextProps;
137
+
138
+ /** Default props passed to every `<View>` component. */
139
+ defaultViewProps?: ViewProps;
140
+
141
+ /** Extra props forwarded to specific tag renderers. */
142
+ renderersProps?: Record<string, Record<string, unknown>>;
143
+
144
+ /** Maximum width for images. */
145
+ maxImagesWidth?: number;
146
+
147
+ /** Placeholder dimensions before image loads. */
148
+ imagesInitialDimensions?: { width: number; height: number };
149
+
150
+ /** Custom bullet/number renderers for lists. */
151
+ listsPrefixesRenderers?: ListsPrefixesRenderers;
152
+
153
+ /** Base em unit in pixels (default 14). */
154
+ emSize?: number;
155
+
156
+ /** List of available system fonts. */
157
+ systemFonts?: string[];
158
+
159
+ /** Map unsupported font families to fallback fonts. */
160
+ fallbackFonts?: Record<string, string>;
161
+
162
+ /** Log parsed DOM and computed styles to the console. */
163
+ debug?: boolean;
164
+
165
+ // --- Security ---
166
+
167
+ /**
168
+ * When false (default), dangerous tags (`<script>`, `<iframe>`, `<object>`,
169
+ * `<embed>`, `<form>`) and `javascript:` hrefs are stripped automatically.
170
+ * Set to true to allow all HTML through (use with caution).
171
+ */
172
+ allowDangerousHtml?: boolean;
173
+
174
+ // --- Dark Mode ---
175
+
176
+ /** Per-tag style overrides applied when the system is in dark mode. */
177
+ darkModeStyles?: TagsStyles;
178
+
179
+ /**
180
+ * Override color scheme detection. When omitted the system scheme is used
181
+ * via `useColorScheme()`.
182
+ */
183
+ colorScheme?: ColorSchemeName;
184
+
185
+ // --- Font Scaling ---
186
+
187
+ /** Allow system font-size accessibility scaling (default true). */
188
+ allowFontScaling?: boolean;
189
+
190
+ /** Cap the font-size multiplier for accessibility scaling. */
191
+ maxFontSizeMultiplier?: number;
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Internal Context
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /** Shape of the context shared through the renderer tree. */
199
+ export interface HtmlRendererContextValue {
200
+ contentWidth: number;
201
+ tagsStyles: TagsStyles;
202
+ classesStyles: ClassesStyles;
203
+ idsStyles: IdsStyles;
204
+ customRenderers: Record<string, CustomRenderer>;
205
+ onLinkPress?: (href: string, attributes: Record<string, string>) => void;
206
+ onImagePress?: (src: string, attributes: Record<string, string>) => void;
207
+ renderersProps: Record<string, Record<string, unknown>>;
208
+ emSize: number;
209
+ debug: boolean;
210
+ ignoredTags: Set<string>;
211
+ ignoredStyles: Set<string>;
212
+ allowedStyles: Set<string> | null;
213
+ defaultTextProps?: TextProps;
214
+ defaultViewProps?: ViewProps;
215
+ maxImagesWidth?: number;
216
+ imagesInitialDimensions: { width: number; height: number };
217
+ listsPrefixesRenderers?: ListsPrefixesRenderers;
218
+ systemFonts?: string[];
219
+ fallbackFonts?: Record<string, string>;
220
+ nestLevel: number;
221
+ /** Resolved color scheme — 'dark' | 'light' | null | undefined. */
222
+ colorScheme: ColorSchemeName | null | undefined;
223
+ /** Dark mode tag style overrides (merged into tagsStyles when dark). */
224
+ darkModeStyles: TagsStyles;
225
+ /** Allow system font scaling on Text components. */
226
+ allowFontScaling: boolean;
227
+ /** Cap the font size multiplier. */
228
+ maxFontSizeMultiplier?: number;
229
+ }
@@ -0,0 +1,132 @@
1
+ import type { AccessibilityRole } from 'react-native';
2
+ import type { DOMElement } from '../types';
3
+ import { extractTextContent } from './index';
4
+
5
+ /** Map HTML tags to React Native accessibility roles. */
6
+ const TAG_TO_ROLE: Record<string, AccessibilityRole> = {
7
+ a: 'link',
8
+ button: 'button',
9
+ img: 'image',
10
+ h1: 'header',
11
+ h2: 'header',
12
+ h3: 'header',
13
+ h4: 'header',
14
+ h5: 'header',
15
+ h6: 'header',
16
+ input: 'none',
17
+ textarea: 'none',
18
+ nav: 'menu',
19
+ form: 'none',
20
+ summary: 'button',
21
+ };
22
+
23
+ /** Accessibility props derived from a DOM element's attributes. */
24
+ export interface A11yProps {
25
+ accessibilityRole?: AccessibilityRole;
26
+ accessibilityLabel?: string;
27
+ accessibilityHint?: string;
28
+ accessibilityState?: {
29
+ disabled?: boolean;
30
+ selected?: boolean;
31
+ checked?: boolean | 'mixed';
32
+ busy?: boolean;
33
+ expanded?: boolean;
34
+ };
35
+ accessible?: boolean;
36
+ importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants';
37
+ }
38
+
39
+ /**
40
+ * Extract accessibility props from a DOMElement's HTML attributes.
41
+ * Supports: aria-label, aria-hidden, aria-role, role, alt, title,
42
+ * aria-disabled, aria-selected, aria-checked, aria-busy, aria-expanded.
43
+ */
44
+ export function getAccessibilityProps(node: DOMElement): A11yProps {
45
+ const attrs = node.attributes;
46
+ const props: A11yProps = {};
47
+
48
+ // Role: aria-role > role attribute > tag-based default
49
+ const ariaRole = attrs['aria-role'] ?? attrs.role;
50
+ if (ariaRole) {
51
+ props.accessibilityRole = ariaRole as AccessibilityRole;
52
+ } else if (TAG_TO_ROLE[node.tag]) {
53
+ props.accessibilityRole = TAG_TO_ROLE[node.tag];
54
+ }
55
+
56
+ // Label: aria-label > alt > title > text content for certain tags
57
+ const ariaLabel = attrs['aria-label'];
58
+ if (ariaLabel) {
59
+ props.accessibilityLabel = ariaLabel;
60
+ } else if (attrs.alt) {
61
+ props.accessibilityLabel = attrs.alt;
62
+ } else if (attrs.title) {
63
+ props.accessibilityLabel = attrs.title;
64
+ }
65
+
66
+ // Hint: for links, use href as hint
67
+ if (node.tag === 'a' && attrs.href) {
68
+ props.accessibilityHint = `Opens ${attrs.href}`;
69
+ }
70
+
71
+ // Hidden: aria-hidden
72
+ if (attrs['aria-hidden'] === 'true') {
73
+ props.importantForAccessibility = 'no-hide-descendants';
74
+ props.accessible = false;
75
+ }
76
+
77
+ // Accessibility state
78
+ const state: A11yProps['accessibilityState'] = {};
79
+ let hasState = false;
80
+
81
+ if (attrs['aria-disabled'] === 'true' || attrs.disabled != null) {
82
+ state.disabled = true;
83
+ hasState = true;
84
+ }
85
+ if (attrs['aria-selected'] === 'true') {
86
+ state.selected = true;
87
+ hasState = true;
88
+ }
89
+ if (attrs['aria-checked'] != null) {
90
+ state.checked =
91
+ attrs['aria-checked'] === 'mixed'
92
+ ? 'mixed'
93
+ : attrs['aria-checked'] === 'true';
94
+ hasState = true;
95
+ }
96
+ if (attrs['aria-busy'] === 'true') {
97
+ state.busy = true;
98
+ hasState = true;
99
+ }
100
+ if (attrs['aria-expanded'] != null) {
101
+ state.expanded = attrs['aria-expanded'] === 'true';
102
+ hasState = true;
103
+ }
104
+
105
+ if (hasState) {
106
+ props.accessibilityState = state;
107
+ }
108
+
109
+ return props;
110
+ }
111
+
112
+ /**
113
+ * Build an accessibility label for an image from its attributes.
114
+ */
115
+ export function getImageA11yLabel(node: DOMElement): string | undefined {
116
+ return (
117
+ node.attributes['aria-label'] ??
118
+ node.attributes.alt ??
119
+ node.attributes.title ??
120
+ undefined
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Build an accessibility label for a link from its children text.
126
+ */
127
+ export function getLinkA11yLabel(node: DOMElement): string | undefined {
128
+ const ariaLabel = node.attributes['aria-label'];
129
+ if (ariaLabel) return ariaLabel;
130
+ const textContent = extractTextContent(node.children);
131
+ return textContent || undefined;
132
+ }
@@ -0,0 +1,83 @@
1
+ import type { DOMNode } from '../types';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Image Dimension Cache (LRU, bounded)
5
+ // ---------------------------------------------------------------------------
6
+
7
+ interface ImageDimensions {
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ const imageDimensionCache = new Map<string, ImageDimensions>();
13
+ const IMAGE_CACHE_MAX_SIZE = 200;
14
+
15
+ /** Get cached image dimensions for a URL. */
16
+ export function getCachedImageDimensions(
17
+ uri: string
18
+ ): ImageDimensions | undefined {
19
+ return imageDimensionCache.get(uri);
20
+ }
21
+
22
+ /** Cache image dimensions for a URL. */
23
+ export function setCachedImageDimensions(
24
+ uri: string,
25
+ dimensions: ImageDimensions
26
+ ): void {
27
+ if (imageDimensionCache.size >= IMAGE_CACHE_MAX_SIZE) {
28
+ const firstKey = imageDimensionCache.keys().next().value;
29
+ if (firstKey !== undefined) {
30
+ imageDimensionCache.delete(firstKey);
31
+ }
32
+ }
33
+ imageDimensionCache.set(uri, dimensions);
34
+ }
35
+
36
+ /** Clear the image dimension cache (useful for testing). */
37
+ export function clearImageDimensionCache(): void {
38
+ imageDimensionCache.clear();
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Parsed DOM Cache (LRU, bounded)
43
+ // Cache key includes html + allowDangerousHtml + ignoredTags to prevent
44
+ // cache poisoning when these parameters change.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const domCache = new Map<string, DOMNode[]>();
48
+ const DOM_CACHE_MAX_SIZE = 50;
49
+
50
+ /**
51
+ * Build a composite cache key that incorporates all parameters affecting
52
+ * the parse/sanitize result, preventing cache poisoning.
53
+ */
54
+ export function buildDOMCacheKey(
55
+ html: string,
56
+ allowDangerousHtml: boolean,
57
+ ignoredTags: Set<string>
58
+ ): string {
59
+ const tagsSuffix =
60
+ ignoredTags.size > 0 ? [...ignoredTags].sort().join(',') : '';
61
+ return `${allowDangerousHtml ? '1' : '0'}|${tagsSuffix}|${html}`;
62
+ }
63
+
64
+ /** Get cached parsed DOM for a cache key. */
65
+ export function getCachedDOM(cacheKey: string): DOMNode[] | undefined {
66
+ return domCache.get(cacheKey);
67
+ }
68
+
69
+ /** Cache parsed DOM for a cache key. */
70
+ export function setCachedDOM(cacheKey: string, nodes: DOMNode[]): void {
71
+ if (domCache.size >= DOM_CACHE_MAX_SIZE) {
72
+ const firstKey = domCache.keys().next().value;
73
+ if (firstKey !== undefined) {
74
+ domCache.delete(firstKey);
75
+ }
76
+ }
77
+ domCache.set(cacheKey, nodes);
78
+ }
79
+
80
+ /** Clear the DOM cache (useful for testing). */
81
+ export function clearDOMCache(): void {
82
+ domCache.clear();
83
+ }
@@ -0,0 +1,151 @@
1
+ import type { DOMNode, DOMElement } from '../types';
2
+
3
+ // Re-export sub-modules
4
+ export { sanitizeDOM } from './sanitize';
5
+ export {
6
+ getAccessibilityProps,
7
+ getImageA11yLabel,
8
+ getLinkA11yLabel,
9
+ type A11yProps,
10
+ } from './accessibility';
11
+ export {
12
+ getCachedImageDimensions,
13
+ setCachedImageDimensions,
14
+ clearImageDimensionCache,
15
+ getCachedDOM,
16
+ setCachedDOM,
17
+ clearDOMCache,
18
+ buildDOMCacheKey,
19
+ } from './cache';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // CSS unit conversion
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Convert a CSS value string to a numeric pixel value.
27
+ * Supports: px, em, rem, bare numbers. Percentage returns NaN (handled upstream).
28
+ */
29
+ export function cssValueToNumeric(
30
+ value: string,
31
+ emSize: number
32
+ ): number | undefined {
33
+ const trimmed = value.trim();
34
+ if (trimmed.endsWith('px')) {
35
+ return parseFloat(trimmed);
36
+ }
37
+ if (trimmed.endsWith('em') || trimmed.endsWith('rem')) {
38
+ return parseFloat(trimmed) * emSize;
39
+ }
40
+ if (trimmed.endsWith('%')) {
41
+ // Percentage — return undefined, caller must handle contextually
42
+ return undefined;
43
+ }
44
+ const num = parseFloat(trimmed);
45
+ return isNaN(num) ? undefined : num;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // DOM helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /** Tags that are inherently inline (text-level). */
53
+ export const INLINE_TAGS = new Set([
54
+ 'span',
55
+ 'strong',
56
+ 'b',
57
+ 'em',
58
+ 'i',
59
+ 'u',
60
+ 's',
61
+ 'strike',
62
+ 'del',
63
+ 'ins',
64
+ 'mark',
65
+ 'small',
66
+ 'sub',
67
+ 'sup',
68
+ 'a',
69
+ 'code',
70
+ 'br',
71
+ ]);
72
+
73
+ /** Tags that are text containers (rendered as `<Text>` at block level). */
74
+ export const TEXT_BLOCK_TAGS = new Set([
75
+ 'p',
76
+ 'h1',
77
+ 'h2',
78
+ 'h3',
79
+ 'h4',
80
+ 'h5',
81
+ 'h6',
82
+ 'li',
83
+ 'th',
84
+ 'td',
85
+ 'pre',
86
+ 'label',
87
+ 'dt',
88
+ 'dd',
89
+ ]);
90
+
91
+ /** Tags that should be silently skipped (no output). */
92
+ export const ALWAYS_IGNORED_TAGS = new Set([
93
+ 'script',
94
+ 'style',
95
+ 'head',
96
+ 'meta',
97
+ 'link',
98
+ 'title',
99
+ 'noscript',
100
+ ]);
101
+
102
+ /**
103
+ * Returns true if every child of the given list is either a text node
104
+ * or an inline-level element whose descendants are also all inline.
105
+ */
106
+ export function isInlineContent(nodes: DOMNode[]): boolean {
107
+ return nodes.every((node) => {
108
+ if (node.type === 'text') return true;
109
+ if (node.type === 'element') {
110
+ return INLINE_TAGS.has(node.tag) && isInlineContent(node.children);
111
+ }
112
+ return false;
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Resolve a potentially relative URL against a base URL.
118
+ */
119
+ export function resolveUrl(url: string, baseUrl?: string): string {
120
+ if (!baseUrl || /^https?:\/\//i.test(url) || url.startsWith('data:')) {
121
+ return url;
122
+ }
123
+ const base = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
124
+ return url.startsWith('/') ? base + url.slice(1) : base + url;
125
+ }
126
+
127
+ /**
128
+ * Type-guard: checks if a DOMNode is a DOMElement.
129
+ */
130
+ export function isDOMElement(node: DOMNode): node is DOMElement {
131
+ return node.type === 'element';
132
+ }
133
+
134
+ /**
135
+ * Extract all text content from a node tree (for accessibility labels, etc.).
136
+ */
137
+ export function extractTextContent(nodes: DOMNode[]): string {
138
+ let text = '';
139
+ for (const node of nodes) {
140
+ if (node.type === 'text') {
141
+ text += node.data;
142
+ } else if (node.type === 'element') {
143
+ if (node.tag === 'br') {
144
+ text += '\n';
145
+ } else {
146
+ text += extractTextContent(node.children);
147
+ }
148
+ }
149
+ }
150
+ return text;
151
+ }