@transferwise/components 0.0.0-experimental-fa3aa98 → 0.0.0-experimental-f4a328f
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/build/Calendar/Calendar.js +16 -94
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.messages.js +0 -12
- package/build/Calendar/Calendar.messages.js.map +1 -1
- package/build/Calendar/Calendar.messages.mjs +0 -12
- package/build/Calendar/Calendar.messages.mjs.map +1 -1
- package/build/Calendar/Calendar.mjs +17 -95
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.js +1 -5
- package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.mjs +1 -5
- package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.js +0 -14
- package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.mjs +0 -14
- package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +19 -53
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +19 -53
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.js +0 -6
- package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs +0 -6
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +3 -8
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +3 -8
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +3 -8
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +3 -8
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.js +0 -7
- package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.mjs +0 -7
- package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
- package/build/Calendar/{TableLink → tableLink}/TableLink.js +11 -26
- package/build/Calendar/tableLink/TableLink.js.map +1 -0
- package/build/Calendar/{TableLink → tableLink}/TableLink.mjs +11 -26
- package/build/Calendar/tableLink/TableLink.mjs.map +1 -0
- package/build/DateLookup/DateLookup.js +9 -20
- package/build/DateLookup/DateLookup.js.map +1 -1
- package/build/DateLookup/DateLookup.mjs +9 -20
- package/build/DateLookup/DateLookup.mjs.map +1 -1
- package/build/Markup/Markup.js +137 -0
- package/build/Markup/Markup.js.map +1 -0
- package/build/Markup/Markup.mjs +132 -0
- package/build/Markup/Markup.mjs.map +1 -0
- package/build/Markup/constants.js +36 -0
- package/build/Markup/constants.js.map +1 -0
- package/build/Markup/constants.mjs +29 -0
- package/build/Markup/constants.mjs.map +1 -0
- package/build/Markup/utils/parseMarkup.js +236 -0
- package/build/Markup/utils/parseMarkup.js.map +1 -0
- package/build/Markup/utils/parseMarkup.mjs +231 -0
- package/build/Markup/utils/parseMarkup.mjs.map +1 -0
- package/build/Markup/utils/sanitise.js +114 -0
- package/build/Markup/utils/sanitise.js.map +1 -0
- package/build/Markup/utils/sanitise.mjs +111 -0
- package/build/Markup/utils/sanitise.mjs.map +1 -0
- package/build/i18n/en.json +0 -4
- package/build/i18n/en.json.js +0 -4
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +0 -4
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/index.mjs +2 -1
- package/build/index.mjs.map +1 -1
- package/build/main.css +78 -123
- package/build/styles/Calendar/Calendar.css +37 -112
- package/build/styles/Markup/Markup.css +28 -0
- package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/build/styles/main.css +78 -123
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/Calendar/Calendar.d.ts +3 -16
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.messages.d.ts +0 -20
- package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
- package/build/types/Calendar/DateHeader/DateHeader.d.ts +0 -2
- package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +1 -5
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +0 -4
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +0 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +1 -2
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +1 -2
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +0 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
- package/build/types/Calendar/index.d.ts +0 -1
- package/build/types/Calendar/index.d.ts.map +1 -1
- package/build/types/Calendar/{TableLink → tableLink}/TableLink.d.ts +1 -3
- package/build/types/Calendar/tableLink/TableLink.d.ts.map +1 -0
- package/build/types/Calendar/tableLink/index.d.ts.map +1 -0
- package/build/types/DateLookup/DateLookup.d.ts +5 -24
- package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/DateLookup/index.d.ts +0 -1
- package/build/types/DateLookup/index.d.ts.map +1 -1
- package/build/types/IconButton/IconButton.d.ts +1 -1
- package/build/types/Markup/Markup.d.ts +18 -0
- package/build/types/Markup/Markup.d.ts.map +1 -0
- package/build/types/Markup/Markup.types.d.ts +77 -0
- package/build/types/Markup/Markup.types.d.ts.map +1 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
- package/build/types/Markup/constants.d.ts +20 -0
- package/build/types/Markup/constants.d.ts.map +1 -0
- package/build/types/Markup/index.d.ts +3 -0
- package/build/types/Markup/index.d.ts.map +1 -0
- package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
- package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
- package/build/types/Markup/utils/sanitise.d.ts +4 -0
- package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
- package/build/types/Title/Title.d.ts +2 -2
- package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
- package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
- package/build/types/common/DateUtils/index.d.ts +0 -2
- package/build/types/common/DateUtils/index.d.ts.map +1 -1
- package/build/types/index.d.ts +4 -3
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +0 -8
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Calendar/Calendar.css +37 -112
- package/src/Calendar/Calendar.less +35 -5
- package/src/Calendar/Calendar.messages.ts +0 -21
- package/src/Calendar/Calendar.test.tsx +27 -288
- package/src/Calendar/Calendar.tsx +19 -136
- package/src/Calendar/DateHeader/DateHeader.tsx +0 -6
- package/src/Calendar/DayCalendar/DayCalendar.tsx +1 -33
- package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +28 -62
- package/src/Calendar/MonthCalendar/MonthCalendar.tsx +0 -8
- package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +3 -15
- package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +3 -15
- package/src/Calendar/YearCalendar/YearCalendar.tsx +0 -9
- package/src/Calendar/_stories/Calendar.story.tsx +4 -138
- package/src/Calendar/index.ts +0 -7
- package/src/Calendar/{TableLink → tableLink}/TableLink.tsx +14 -29
- package/src/DateLookup/DateLookup.test.tsx +37 -34
- package/src/DateLookup/DateLookup.tsx +13 -37
- package/src/DateLookup/_stories/DateLookup.story.tsx +2 -124
- package/src/DateLookup/index.ts +0 -6
- package/src/Markup/Markup.css +28 -0
- package/src/Markup/Markup.injection.test.tsx +163 -0
- package/src/Markup/Markup.less +29 -0
- package/src/Markup/Markup.test.tsx +463 -0
- package/src/Markup/Markup.tsx +169 -0
- package/src/Markup/Markup.types.ts +62 -0
- package/src/Markup/_stories/Markup.docs.mdx +75 -0
- package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
- package/src/Markup/_stories/Markup.story.tsx +266 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
- package/src/Markup/constants.ts +41 -0
- package/src/Markup/index.ts +2 -0
- package/src/Markup/utils/parseMarkup.test.ts +706 -0
- package/src/Markup/utils/parseMarkup.ts +234 -0
- package/src/Markup/utils/sanitise.test.ts +497 -0
- package/src/Markup/utils/sanitise.ts +133 -0
- package/src/common/DateUtils/index.ts +0 -2
- package/src/i18n/en.json +0 -4
- package/src/index.ts +4 -3
- package/src/main.css +78 -123
- package/src/main.less +1 -0
- package/build/Calendar/TableLink/TableLink.js.map +0 -1
- package/build/Calendar/TableLink/TableLink.mjs.map +0 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +0 -33
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +0 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +0 -31
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +0 -1
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +0 -40
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +0 -1
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +0 -38
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +0 -1
- package/build/styles/Calendar/TableLink/TableLink.css +0 -115
- package/build/types/Calendar/Calendar.types.d.ts +0 -32
- package/build/types/Calendar/Calendar.types.d.ts.map +0 -1
- package/build/types/Calendar/TableLink/TableLink.d.ts.map +0 -1
- package/build/types/Calendar/TableLink/index.d.ts.map +0 -1
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +0 -3
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +0 -1
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +0 -3
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +0 -1
- package/src/Calendar/Calendar.disabledDates.test.tsx +0 -518
- package/src/Calendar/Calendar.types.ts +0 -35
- package/src/Calendar/TableLink/TableLink.css +0 -115
- package/src/Calendar/TableLink/TableLink.less +0 -68
- package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +0 -118
- package/src/Calendar/_stories/Calendar.test.story.tsx +0 -74
- package/src/DateLookup/DateLookup.disabledDates.test.tsx +0 -293
- package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +0 -108
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +0 -98
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +0 -45
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +0 -169
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +0 -59
- /package/build/types/Calendar/{TableLink → tableLink}/index.d.ts +0 -0
- /package/src/Calendar/{TableLink → tableLink}/index.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseMarkup.js","sources":["../../../src/Markup/utils/parseMarkup.ts"],"sourcesContent":["import { ALLOWED_TARGETS, MAX_NESTING_DEPTH, TAG_ALIASES, TAG_REGEX } from '../constants';\nimport { MarkupNode } from '../Markup.types';\n\nexport { MAX_NESTING_DEPTH };\nexport const MAX_INPUT_LENGTH = 10_000;\nexport const MAX_ACCESSIBILITY_LABEL_LENGTH = 250;\n\nconst OBJECT_PROTOTYPE_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));\n\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f-\\x9f]/g;\nconst HTML_METACHAR = /[<>\"']/g;\n\ntype Token =\n | { type: 'text'; content: string }\n | { type: 'newline' }\n | { type: 'open'; tag: string; attrs: string }\n | { type: 'close'; tag: string };\n\n/**\n * Splits the input string into a flat list of tokens by scanning for known tags.\n * Text between tags is emitted as text/newline tokens. Unrecognised tags are never\n * tokenised — they remain part of the surrounding text content.\n */\nfunction tokenise(input: string): Token[] {\n const tokens: Token[] = [];\n let lastIndex = 0;\n\n TAG_REGEX.lastIndex = 0;\n let match = TAG_REGEX.exec(input);\n\n while (match !== null) {\n if (match.index > lastIndex) {\n pushTextWithNewlines(tokens, input.slice(lastIndex, match.index));\n }\n\n const isClose = match[1] === '/';\n const tag = match[2];\n const attrs = match[3] || '';\n\n if (isClose) {\n tokens.push({ type: 'close', tag });\n } else {\n tokens.push({ type: 'open', tag, attrs });\n }\n\n lastIndex = TAG_REGEX.lastIndex;\n match = TAG_REGEX.exec(input);\n }\n\n if (lastIndex < input.length) {\n pushTextWithNewlines(tokens, input.slice(lastIndex));\n }\n\n return tokens;\n}\n\n/** Splits a text fragment on newline characters, interleaving newline tokens between parts. */\nfunction pushTextWithNewlines(tokens: Token[], text: string): void {\n const parts = text.split('\\n');\n for (let index = 0; index < parts.length; index += 1) {\n if (index > 0) {\n tokens.push({ type: 'newline' });\n }\n if (parts[index]) {\n tokens.push({ type: 'text', content: parts[index] });\n }\n }\n}\n\ntype ParseResult = { closed: true; nodes: MarkupNode[] } | { closed: false; nodes: MarkupNode[] };\n\n/**\n * Recursive descent parser that consumes tokens and builds a MarkupNode tree.\n *\n * When `untilTag` is provided, the function collects nodes until it finds a matching\n * close token — returning `{ closed: true }`. If it exhausts all tokens without finding\n * the close, it returns `{ closed: false }` so the caller can fall back to plain text\n * (graceful degradation for malformed input).\n *\n * The shared `position` object acts as a cursor across recursive calls, avoiding the\n * need to slice or copy the token array.\n */\nfunction parse(\n tokens: Token[],\n position: { index: number },\n untilTag?: string,\n depth = 0,\n): ParseResult {\n const nodes: MarkupNode[] = [];\n\n while (position.index < tokens.length) {\n const token = tokens[position.index];\n\n if (token.type === 'text') {\n nodes.push({ type: 'text', content: token.content });\n position.index += 1;\n } else if (token.type === 'newline') {\n nodes.push({ type: 'newline' });\n position.index += 1;\n } else if (token.type === 'close') {\n if (token.tag === untilTag) {\n position.index += 1;\n return { closed: true, nodes };\n }\n // Mismatched close tag — render as literal text\n nodes.push({ type: 'text', content: `</${token.tag}>` });\n position.index += 1;\n } else if (token.type === 'open') {\n if (depth >= MAX_NESTING_DEPTH) {\n nodes.push({ type: 'text', content: `<${token.tag}${token.attrs}>` });\n position.index += 1;\n } else {\n position.index += 1;\n const result = parse(tokens, position, token.tag, depth + 1);\n\n if (result.closed) {\n const node = buildNode(token.tag, token.attrs, result.nodes);\n nodes.push(node);\n } else {\n nodes.push({ type: 'text', content: `<${token.tag}${token.attrs}>` });\n nodes.push(...result.nodes);\n }\n }\n }\n }\n\n if (untilTag) {\n return { closed: false, nodes };\n }\n\n return { closed: true, nodes };\n}\n\n/**\n * Maps a raw tag name to a typed MarkupNode. Resolves aliases first (p→paragraph, a→link),\n * then builds the appropriate node shape. Link nodes carry full attributes; other element\n * nodes extract only accessibilityLabel.\n */\nfunction buildNode(rawTag: string, attrs: string, children: MarkupNode[]): MarkupNode {\n const tag = TAG_ALIASES[rawTag] || rawTag;\n\n if (tag === 'link') {\n return { type: 'link', children, ...parseAttributes(attrs) };\n }\n\n if (tag === 'paragraph') {\n return { type: 'paragraph', children };\n }\n\n const { accessibilityLabel } = parseAttributes(attrs);\n\n return {\n type: tag as MarkupNode['type'],\n children,\n ...(accessibilityLabel && { accessibilityLabel }),\n } as MarkupNode;\n}\n\nconst ATTR_REGEX = /(?<=\\s)(href|action|target|accessibilityLabel)=(?:\"([^\"]*)\"|'([^']*)')/g;\n\ntype ParsedAttributes = {\n href?: string;\n action?: string;\n target?: string;\n accessibilityLabel?: string;\n};\n\n/**\n * Extracts allowed attributes from a raw attribute string.\n * Supports both double- and single-quoted values. Any attribute not in the\n * allow-list (href, action, target, accessibilityLabel) is silently discarded.\n * First occurrence wins — duplicates are ignored.\n */\nfunction parseAttributes(attrs: string): ParsedAttributes {\n const result: ParsedAttributes = {};\n ATTR_REGEX.lastIndex = 0;\n let match = ATTR_REGEX.exec(attrs);\n\n while (match !== null) {\n const key = match[1] as 'href' | 'action' | 'target' | 'accessibilityLabel';\n const value = match[2] ?? match[3];\n\n if (!(key in result)) {\n const sanitized = sanitizeAttributeValue(key, value);\n if (sanitized !== undefined) {\n result[key] = sanitized;\n }\n }\n\n match = ATTR_REGEX.exec(attrs);\n }\n\n return result;\n}\n\nfunction sanitizeAttributeValue(\n key: 'href' | 'action' | 'target' | 'accessibilityLabel',\n value: string,\n): string | undefined {\n switch (key) {\n case 'href':\n return value.replace(CONTROL_CHARS, '');\n case 'action':\n if (OBJECT_PROTOTYPE_KEYS.has(value)) {\n return undefined;\n }\n return value;\n case 'target':\n if (!ALLOWED_TARGETS.includes(value)) {\n return undefined;\n }\n return value;\n case 'accessibilityLabel':\n return value.replace(HTML_METACHAR, '').slice(0, MAX_ACCESSIBILITY_LABEL_LENGTH);\n default:\n return value;\n }\n}\n\n/** Entry point: tokenises the input string, then parses the token stream into a MarkupNode tree. */\nexport function parseMarkup(input: string): MarkupNode[] {\n if (!input) {\n return [];\n }\n\n if (input.length > MAX_INPUT_LENGTH) {\n return [{ type: 'text', content: input }];\n }\n\n const tokens = tokenise(input);\n const position = { index: 0 };\n return parse(tokens, position).nodes;\n}\n"],"names":["MAX_INPUT_LENGTH","MAX_ACCESSIBILITY_LABEL_LENGTH","OBJECT_PROTOTYPE_KEYS","Set","Object","getOwnPropertyNames","prototype","CONTROL_CHARS","HTML_METACHAR","tokenise","input","tokens","lastIndex","TAG_REGEX","match","exec","index","pushTextWithNewlines","slice","isClose","tag","attrs","push","type","length","text","parts","split","content","parse","position","untilTag","depth","nodes","token","closed","MAX_NESTING_DEPTH","result","node","buildNode","rawTag","children","TAG_ALIASES","parseAttributes","accessibilityLabel","ATTR_REGEX","key","value","sanitized","sanitizeAttributeValue","undefined","replace","has","ALLOWED_TARGETS","includes","parseMarkup"],"mappings":"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"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { TAG_REGEX, MAX_NESTING_DEPTH, TAG_ALIASES, ALLOWED_TARGETS } from '../constants.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_INPUT_LENGTH = 10_000;
|
|
4
|
+
const MAX_ACCESSIBILITY_LABEL_LENGTH = 250;
|
|
5
|
+
const OBJECT_PROTOTYPE_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
6
|
+
// eslint-disable-next-line regexp/no-control-character
|
|
7
|
+
const CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
|
|
8
|
+
const HTML_METACHAR = /[<>"']/g;
|
|
9
|
+
/**
|
|
10
|
+
* Splits the input string into a flat list of tokens by scanning for known tags.
|
|
11
|
+
* Text between tags is emitted as text/newline tokens. Unrecognised tags are never
|
|
12
|
+
* tokenised — they remain part of the surrounding text content.
|
|
13
|
+
*/
|
|
14
|
+
function tokenise(input) {
|
|
15
|
+
const tokens = [];
|
|
16
|
+
let lastIndex = 0;
|
|
17
|
+
TAG_REGEX.lastIndex = 0;
|
|
18
|
+
let match = TAG_REGEX.exec(input);
|
|
19
|
+
while (match !== null) {
|
|
20
|
+
if (match.index > lastIndex) {
|
|
21
|
+
pushTextWithNewlines(tokens, input.slice(lastIndex, match.index));
|
|
22
|
+
}
|
|
23
|
+
const isClose = match[1] === '/';
|
|
24
|
+
const tag = match[2];
|
|
25
|
+
const attrs = match[3] || '';
|
|
26
|
+
if (isClose) {
|
|
27
|
+
tokens.push({
|
|
28
|
+
type: 'close',
|
|
29
|
+
tag
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
tokens.push({
|
|
33
|
+
type: 'open',
|
|
34
|
+
tag,
|
|
35
|
+
attrs
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
lastIndex = TAG_REGEX.lastIndex;
|
|
39
|
+
match = TAG_REGEX.exec(input);
|
|
40
|
+
}
|
|
41
|
+
if (lastIndex < input.length) {
|
|
42
|
+
pushTextWithNewlines(tokens, input.slice(lastIndex));
|
|
43
|
+
}
|
|
44
|
+
return tokens;
|
|
45
|
+
}
|
|
46
|
+
/** Splits a text fragment on newline characters, interleaving newline tokens between parts. */
|
|
47
|
+
function pushTextWithNewlines(tokens, text) {
|
|
48
|
+
const parts = text.split('\n');
|
|
49
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
50
|
+
if (index > 0) {
|
|
51
|
+
tokens.push({
|
|
52
|
+
type: 'newline'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (parts[index]) {
|
|
56
|
+
tokens.push({
|
|
57
|
+
type: 'text',
|
|
58
|
+
content: parts[index]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Recursive descent parser that consumes tokens and builds a MarkupNode tree.
|
|
65
|
+
*
|
|
66
|
+
* When `untilTag` is provided, the function collects nodes until it finds a matching
|
|
67
|
+
* close token — returning `{ closed: true }`. If it exhausts all tokens without finding
|
|
68
|
+
* the close, it returns `{ closed: false }` so the caller can fall back to plain text
|
|
69
|
+
* (graceful degradation for malformed input).
|
|
70
|
+
*
|
|
71
|
+
* The shared `position` object acts as a cursor across recursive calls, avoiding the
|
|
72
|
+
* need to slice or copy the token array.
|
|
73
|
+
*/
|
|
74
|
+
function parse(tokens, position, untilTag, depth = 0) {
|
|
75
|
+
const nodes = [];
|
|
76
|
+
while (position.index < tokens.length) {
|
|
77
|
+
const token = tokens[position.index];
|
|
78
|
+
if (token.type === 'text') {
|
|
79
|
+
nodes.push({
|
|
80
|
+
type: 'text',
|
|
81
|
+
content: token.content
|
|
82
|
+
});
|
|
83
|
+
position.index += 1;
|
|
84
|
+
} else if (token.type === 'newline') {
|
|
85
|
+
nodes.push({
|
|
86
|
+
type: 'newline'
|
|
87
|
+
});
|
|
88
|
+
position.index += 1;
|
|
89
|
+
} else if (token.type === 'close') {
|
|
90
|
+
if (token.tag === untilTag) {
|
|
91
|
+
position.index += 1;
|
|
92
|
+
return {
|
|
93
|
+
closed: true,
|
|
94
|
+
nodes
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Mismatched close tag — render as literal text
|
|
98
|
+
nodes.push({
|
|
99
|
+
type: 'text',
|
|
100
|
+
content: `</${token.tag}>`
|
|
101
|
+
});
|
|
102
|
+
position.index += 1;
|
|
103
|
+
} else if (token.type === 'open') {
|
|
104
|
+
if (depth >= MAX_NESTING_DEPTH) {
|
|
105
|
+
nodes.push({
|
|
106
|
+
type: 'text',
|
|
107
|
+
content: `<${token.tag}${token.attrs}>`
|
|
108
|
+
});
|
|
109
|
+
position.index += 1;
|
|
110
|
+
} else {
|
|
111
|
+
position.index += 1;
|
|
112
|
+
const result = parse(tokens, position, token.tag, depth + 1);
|
|
113
|
+
if (result.closed) {
|
|
114
|
+
const node = buildNode(token.tag, token.attrs, result.nodes);
|
|
115
|
+
nodes.push(node);
|
|
116
|
+
} else {
|
|
117
|
+
nodes.push({
|
|
118
|
+
type: 'text',
|
|
119
|
+
content: `<${token.tag}${token.attrs}>`
|
|
120
|
+
});
|
|
121
|
+
nodes.push(...result.nodes);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (untilTag) {
|
|
127
|
+
return {
|
|
128
|
+
closed: false,
|
|
129
|
+
nodes
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
closed: true,
|
|
134
|
+
nodes
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Maps a raw tag name to a typed MarkupNode. Resolves aliases first (p→paragraph, a→link),
|
|
139
|
+
* then builds the appropriate node shape. Link nodes carry full attributes; other element
|
|
140
|
+
* nodes extract only accessibilityLabel.
|
|
141
|
+
*/
|
|
142
|
+
function buildNode(rawTag, attrs, children) {
|
|
143
|
+
const tag = TAG_ALIASES[rawTag] || rawTag;
|
|
144
|
+
if (tag === 'link') {
|
|
145
|
+
return {
|
|
146
|
+
type: 'link',
|
|
147
|
+
children,
|
|
148
|
+
...parseAttributes(attrs)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (tag === 'paragraph') {
|
|
152
|
+
return {
|
|
153
|
+
type: 'paragraph',
|
|
154
|
+
children
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const {
|
|
158
|
+
accessibilityLabel
|
|
159
|
+
} = parseAttributes(attrs);
|
|
160
|
+
return {
|
|
161
|
+
type: tag,
|
|
162
|
+
children,
|
|
163
|
+
...(accessibilityLabel && {
|
|
164
|
+
accessibilityLabel
|
|
165
|
+
})
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const ATTR_REGEX = /(?<=\s)(href|action|target|accessibilityLabel)=(?:"([^"]*)"|'([^']*)')/g;
|
|
169
|
+
/**
|
|
170
|
+
* Extracts allowed attributes from a raw attribute string.
|
|
171
|
+
* Supports both double- and single-quoted values. Any attribute not in the
|
|
172
|
+
* allow-list (href, action, target, accessibilityLabel) is silently discarded.
|
|
173
|
+
* First occurrence wins — duplicates are ignored.
|
|
174
|
+
*/
|
|
175
|
+
function parseAttributes(attrs) {
|
|
176
|
+
const result = {};
|
|
177
|
+
ATTR_REGEX.lastIndex = 0;
|
|
178
|
+
let match = ATTR_REGEX.exec(attrs);
|
|
179
|
+
while (match !== null) {
|
|
180
|
+
const key = match[1];
|
|
181
|
+
const value = match[2] ?? match[3];
|
|
182
|
+
if (!(key in result)) {
|
|
183
|
+
const sanitized = sanitizeAttributeValue(key, value);
|
|
184
|
+
if (sanitized !== undefined) {
|
|
185
|
+
result[key] = sanitized;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
match = ATTR_REGEX.exec(attrs);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function sanitizeAttributeValue(key, value) {
|
|
193
|
+
switch (key) {
|
|
194
|
+
case 'href':
|
|
195
|
+
return value.replace(CONTROL_CHARS, '');
|
|
196
|
+
case 'action':
|
|
197
|
+
if (OBJECT_PROTOTYPE_KEYS.has(value)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
case 'target':
|
|
202
|
+
if (!ALLOWED_TARGETS.includes(value)) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
case 'accessibilityLabel':
|
|
207
|
+
return value.replace(HTML_METACHAR, '').slice(0, MAX_ACCESSIBILITY_LABEL_LENGTH);
|
|
208
|
+
default:
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Entry point: tokenises the input string, then parses the token stream into a MarkupNode tree. */
|
|
213
|
+
function parseMarkup(input) {
|
|
214
|
+
if (!input) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
if (input.length > MAX_INPUT_LENGTH) {
|
|
218
|
+
return [{
|
|
219
|
+
type: 'text',
|
|
220
|
+
content: input
|
|
221
|
+
}];
|
|
222
|
+
}
|
|
223
|
+
const tokens = tokenise(input);
|
|
224
|
+
const position = {
|
|
225
|
+
index: 0
|
|
226
|
+
};
|
|
227
|
+
return parse(tokens, position).nodes;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { MAX_ACCESSIBILITY_LABEL_LENGTH, MAX_INPUT_LENGTH, MAX_NESTING_DEPTH, parseMarkup };
|
|
231
|
+
//# sourceMappingURL=parseMarkup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseMarkup.mjs","sources":["../../../src/Markup/utils/parseMarkup.ts"],"sourcesContent":["import { ALLOWED_TARGETS, MAX_NESTING_DEPTH, TAG_ALIASES, TAG_REGEX } from '../constants';\nimport { MarkupNode } from '../Markup.types';\n\nexport { MAX_NESTING_DEPTH };\nexport const MAX_INPUT_LENGTH = 10_000;\nexport const MAX_ACCESSIBILITY_LABEL_LENGTH = 250;\n\nconst OBJECT_PROTOTYPE_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));\n\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f-\\x9f]/g;\nconst HTML_METACHAR = /[<>\"']/g;\n\ntype Token =\n | { type: 'text'; content: string }\n | { type: 'newline' }\n | { type: 'open'; tag: string; attrs: string }\n | { type: 'close'; tag: string };\n\n/**\n * Splits the input string into a flat list of tokens by scanning for known tags.\n * Text between tags is emitted as text/newline tokens. Unrecognised tags are never\n * tokenised — they remain part of the surrounding text content.\n */\nfunction tokenise(input: string): Token[] {\n const tokens: Token[] = [];\n let lastIndex = 0;\n\n TAG_REGEX.lastIndex = 0;\n let match = TAG_REGEX.exec(input);\n\n while (match !== null) {\n if (match.index > lastIndex) {\n pushTextWithNewlines(tokens, input.slice(lastIndex, match.index));\n }\n\n const isClose = match[1] === '/';\n const tag = match[2];\n const attrs = match[3] || '';\n\n if (isClose) {\n tokens.push({ type: 'close', tag });\n } else {\n tokens.push({ type: 'open', tag, attrs });\n }\n\n lastIndex = TAG_REGEX.lastIndex;\n match = TAG_REGEX.exec(input);\n }\n\n if (lastIndex < input.length) {\n pushTextWithNewlines(tokens, input.slice(lastIndex));\n }\n\n return tokens;\n}\n\n/** Splits a text fragment on newline characters, interleaving newline tokens between parts. */\nfunction pushTextWithNewlines(tokens: Token[], text: string): void {\n const parts = text.split('\\n');\n for (let index = 0; index < parts.length; index += 1) {\n if (index > 0) {\n tokens.push({ type: 'newline' });\n }\n if (parts[index]) {\n tokens.push({ type: 'text', content: parts[index] });\n }\n }\n}\n\ntype ParseResult = { closed: true; nodes: MarkupNode[] } | { closed: false; nodes: MarkupNode[] };\n\n/**\n * Recursive descent parser that consumes tokens and builds a MarkupNode tree.\n *\n * When `untilTag` is provided, the function collects nodes until it finds a matching\n * close token — returning `{ closed: true }`. If it exhausts all tokens without finding\n * the close, it returns `{ closed: false }` so the caller can fall back to plain text\n * (graceful degradation for malformed input).\n *\n * The shared `position` object acts as a cursor across recursive calls, avoiding the\n * need to slice or copy the token array.\n */\nfunction parse(\n tokens: Token[],\n position: { index: number },\n untilTag?: string,\n depth = 0,\n): ParseResult {\n const nodes: MarkupNode[] = [];\n\n while (position.index < tokens.length) {\n const token = tokens[position.index];\n\n if (token.type === 'text') {\n nodes.push({ type: 'text', content: token.content });\n position.index += 1;\n } else if (token.type === 'newline') {\n nodes.push({ type: 'newline' });\n position.index += 1;\n } else if (token.type === 'close') {\n if (token.tag === untilTag) {\n position.index += 1;\n return { closed: true, nodes };\n }\n // Mismatched close tag — render as literal text\n nodes.push({ type: 'text', content: `</${token.tag}>` });\n position.index += 1;\n } else if (token.type === 'open') {\n if (depth >= MAX_NESTING_DEPTH) {\n nodes.push({ type: 'text', content: `<${token.tag}${token.attrs}>` });\n position.index += 1;\n } else {\n position.index += 1;\n const result = parse(tokens, position, token.tag, depth + 1);\n\n if (result.closed) {\n const node = buildNode(token.tag, token.attrs, result.nodes);\n nodes.push(node);\n } else {\n nodes.push({ type: 'text', content: `<${token.tag}${token.attrs}>` });\n nodes.push(...result.nodes);\n }\n }\n }\n }\n\n if (untilTag) {\n return { closed: false, nodes };\n }\n\n return { closed: true, nodes };\n}\n\n/**\n * Maps a raw tag name to a typed MarkupNode. Resolves aliases first (p→paragraph, a→link),\n * then builds the appropriate node shape. Link nodes carry full attributes; other element\n * nodes extract only accessibilityLabel.\n */\nfunction buildNode(rawTag: string, attrs: string, children: MarkupNode[]): MarkupNode {\n const tag = TAG_ALIASES[rawTag] || rawTag;\n\n if (tag === 'link') {\n return { type: 'link', children, ...parseAttributes(attrs) };\n }\n\n if (tag === 'paragraph') {\n return { type: 'paragraph', children };\n }\n\n const { accessibilityLabel } = parseAttributes(attrs);\n\n return {\n type: tag as MarkupNode['type'],\n children,\n ...(accessibilityLabel && { accessibilityLabel }),\n } as MarkupNode;\n}\n\nconst ATTR_REGEX = /(?<=\\s)(href|action|target|accessibilityLabel)=(?:\"([^\"]*)\"|'([^']*)')/g;\n\ntype ParsedAttributes = {\n href?: string;\n action?: string;\n target?: string;\n accessibilityLabel?: string;\n};\n\n/**\n * Extracts allowed attributes from a raw attribute string.\n * Supports both double- and single-quoted values. Any attribute not in the\n * allow-list (href, action, target, accessibilityLabel) is silently discarded.\n * First occurrence wins — duplicates are ignored.\n */\nfunction parseAttributes(attrs: string): ParsedAttributes {\n const result: ParsedAttributes = {};\n ATTR_REGEX.lastIndex = 0;\n let match = ATTR_REGEX.exec(attrs);\n\n while (match !== null) {\n const key = match[1] as 'href' | 'action' | 'target' | 'accessibilityLabel';\n const value = match[2] ?? match[3];\n\n if (!(key in result)) {\n const sanitized = sanitizeAttributeValue(key, value);\n if (sanitized !== undefined) {\n result[key] = sanitized;\n }\n }\n\n match = ATTR_REGEX.exec(attrs);\n }\n\n return result;\n}\n\nfunction sanitizeAttributeValue(\n key: 'href' | 'action' | 'target' | 'accessibilityLabel',\n value: string,\n): string | undefined {\n switch (key) {\n case 'href':\n return value.replace(CONTROL_CHARS, '');\n case 'action':\n if (OBJECT_PROTOTYPE_KEYS.has(value)) {\n return undefined;\n }\n return value;\n case 'target':\n if (!ALLOWED_TARGETS.includes(value)) {\n return undefined;\n }\n return value;\n case 'accessibilityLabel':\n return value.replace(HTML_METACHAR, '').slice(0, MAX_ACCESSIBILITY_LABEL_LENGTH);\n default:\n return value;\n }\n}\n\n/** Entry point: tokenises the input string, then parses the token stream into a MarkupNode tree. */\nexport function parseMarkup(input: string): MarkupNode[] {\n if (!input) {\n return [];\n }\n\n if (input.length > MAX_INPUT_LENGTH) {\n return [{ type: 'text', content: input }];\n }\n\n const tokens = tokenise(input);\n const position = { index: 0 };\n return parse(tokens, position).nodes;\n}\n"],"names":["MAX_INPUT_LENGTH","MAX_ACCESSIBILITY_LABEL_LENGTH","OBJECT_PROTOTYPE_KEYS","Set","Object","getOwnPropertyNames","prototype","CONTROL_CHARS","HTML_METACHAR","tokenise","input","tokens","lastIndex","TAG_REGEX","match","exec","index","pushTextWithNewlines","slice","isClose","tag","attrs","push","type","length","text","parts","split","content","parse","position","untilTag","depth","nodes","token","closed","MAX_NESTING_DEPTH","result","node","buildNode","rawTag","children","TAG_ALIASES","parseAttributes","accessibilityLabel","ATTR_REGEX","key","value","sanitized","sanitizeAttributeValue","undefined","replace","has","ALLOWED_TARGETS","includes","parseMarkup"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var constants = require('../constants.js');
|
|
4
|
+
|
|
5
|
+
const INVISIBLE_CHARS = /[\u200b\u200c\u200d\ufeff\u00ad\u2060\u180e]/g;
|
|
6
|
+
// eslint-disable-next-line regexp/no-control-character
|
|
7
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
8
|
+
const DANGEROUS_HTML_CHARS = /[<>"']/;
|
|
9
|
+
const DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\s*:/i;
|
|
10
|
+
// eslint-disable-next-line regexp/no-useless-non-capturing-group
|
|
11
|
+
const ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;
|
|
12
|
+
const IPV6_BRACKETS = /^\[|\]$/g;
|
|
13
|
+
const NUMERIC_IP = /^[\d.]+$/;
|
|
14
|
+
const HEX_IP = /^0x[0-9a-f]+$/i;
|
|
15
|
+
function isAllowedHref(href) {
|
|
16
|
+
const cleaned = href.replace(INVISIBLE_CHARS, '');
|
|
17
|
+
const trimmed = cleaned.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (CONTROL_CHARS.test(trimmed)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (DANGEROUS_HTML_CHARS.test(trimmed)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (ENCODED_COLON_SCHEME.test(trimmed)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (trimmed.startsWith('//')) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.startsWith('\\')) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (trimmed.startsWith('/')) {
|
|
40
|
+
return trimmed[1] !== '\\';
|
|
41
|
+
}
|
|
42
|
+
if (trimmed.startsWith('#')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(trimmed);
|
|
50
|
+
if (!constants.ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (url.protocol === 'https:') {
|
|
54
|
+
if (!trimmed.startsWith('https://')) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!url.hostname) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (url.username || url.password) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (isInternalHost(url.hostname)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (url.protocol === 'mailto:') {
|
|
68
|
+
if (url.search) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (!url.pathname) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Synthetic origin used to resolve relative paths via the WHATWG URL parser.
|
|
81
|
+
// If the resolved origin still equals this sentinel, the path stays on-origin.
|
|
82
|
+
const SSR_SENTINEL_ORIGIN = 'https://internal.invalid';
|
|
83
|
+
function isHrefAllowedByMode(href, mode) {
|
|
84
|
+
switch (mode) {
|
|
85
|
+
case 'none':
|
|
86
|
+
return false;
|
|
87
|
+
case 'all':
|
|
88
|
+
return true;
|
|
89
|
+
case 'internal':
|
|
90
|
+
if (href.startsWith('https://wise.com/')) return true;
|
|
91
|
+
if (href.startsWith('/')) {
|
|
92
|
+
try {
|
|
93
|
+
return new URL(href, SSR_SENTINEL_ORIGIN).origin === SSR_SENTINEL_ORIGIN;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isInternalHost(hostname) {
|
|
102
|
+
const stripped = hostname.replace(IPV6_BRACKETS, '');
|
|
103
|
+
const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;
|
|
104
|
+
if (NUMERIC_IP.test(host)) return true;
|
|
105
|
+
if (HEX_IP.test(host)) return true;
|
|
106
|
+
if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;
|
|
107
|
+
if (host === 'localhost') return true;
|
|
108
|
+
if (host.endsWith('.internal')) return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exports.isAllowedHref = isAllowedHref;
|
|
113
|
+
exports.isHrefAllowedByMode = isHrefAllowedByMode;
|
|
114
|
+
//# sourceMappingURL=sanitise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitise.js","sources":["../../../src/Markup/utils/sanitise.ts"],"sourcesContent":["import { ALLOWED_PROTOCOLS } from '../constants';\n\nconst INVISIBLE_CHARS = /[\\u200b\\u200c\\u200d\\ufeff\\u00ad\\u2060\\u180e]/g;\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/;\nconst DANGEROUS_HTML_CHARS = /[<>\"']/;\nconst DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\\s*:/i;\n// eslint-disable-next-line regexp/no-useless-non-capturing-group\nconst ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;\nconst IPV6_BRACKETS = /^\\[|\\]$/g;\nconst NUMERIC_IP = /^[\\d.]+$/;\nconst HEX_IP = /^0x[0-9a-f]+$/i;\n\nexport function isAllowedHref(href: string): boolean {\n const cleaned = href.replace(INVISIBLE_CHARS, '');\n const trimmed = cleaned.trim();\n\n if (!trimmed) {\n return false;\n }\n\n if (CONTROL_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_HTML_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (ENCODED_COLON_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith('//')) {\n return false;\n }\n\n if (trimmed.startsWith('\\\\')) {\n return false;\n }\n\n if (trimmed.startsWith('/')) {\n return trimmed[1] !== '\\\\';\n }\n\n if (trimmed.startsWith('#')) {\n return true;\n }\n\n if (trimmed.startsWith('./') || trimmed.startsWith('../')) {\n return false;\n }\n\n try {\n const url = new URL(trimmed);\n\n if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {\n return false;\n }\n\n if (url.protocol === 'https:') {\n if (!trimmed.startsWith('https://')) {\n return false;\n }\n\n if (!url.hostname) {\n return false;\n }\n\n if (url.username || url.password) {\n return false;\n }\n\n if (isInternalHost(url.hostname)) {\n return false;\n }\n }\n\n if (url.protocol === 'mailto:') {\n if (url.search) {\n return false;\n }\n\n if (!url.pathname) {\n return false;\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport type AllowLinksMode = 'internal' | 'all' | 'none';\n\n// Synthetic origin used to resolve relative paths via the WHATWG URL parser.\n// If the resolved origin still equals this sentinel, the path stays on-origin.\nconst SSR_SENTINEL_ORIGIN = 'https://internal.invalid';\n\nexport function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean {\n switch (mode) {\n case 'none':\n return false;\n case 'all':\n return true;\n case 'internal':\n if (href.startsWith('https://wise.com/')) return true;\n if (href.startsWith('/')) {\n try {\n return new URL(href, SSR_SENTINEL_ORIGIN).origin === SSR_SENTINEL_ORIGIN;\n } catch {\n return false;\n }\n }\n return false;\n }\n}\n\nfunction isInternalHost(hostname: string): boolean {\n const stripped = hostname.replace(IPV6_BRACKETS, '');\n const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;\n if (NUMERIC_IP.test(host)) return true;\n if (HEX_IP.test(host)) return true;\n if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;\n if (host === 'localhost') return true;\n if (host.endsWith('.internal')) return true;\n return false;\n}\n"],"names":["INVISIBLE_CHARS","CONTROL_CHARS","DANGEROUS_HTML_CHARS","DANGEROUS_SCHEME","ENCODED_COLON_SCHEME","IPV6_BRACKETS","NUMERIC_IP","HEX_IP","isAllowedHref","href","cleaned","replace","trimmed","trim","test","startsWith","url","URL","ALLOWED_PROTOCOLS","includes","protocol","hostname","username","password","isInternalHost","search","pathname","SSR_SENTINEL_ORIGIN","isHrefAllowedByMode","mode","origin","stripped","host","endsWith","slice"],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ALLOWED_PROTOCOLS } from '../constants.mjs';
|
|
2
|
+
|
|
3
|
+
const INVISIBLE_CHARS = /[\u200b\u200c\u200d\ufeff\u00ad\u2060\u180e]/g;
|
|
4
|
+
// eslint-disable-next-line regexp/no-control-character
|
|
5
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
6
|
+
const DANGEROUS_HTML_CHARS = /[<>"']/;
|
|
7
|
+
const DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\s*:/i;
|
|
8
|
+
// eslint-disable-next-line regexp/no-useless-non-capturing-group
|
|
9
|
+
const ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;
|
|
10
|
+
const IPV6_BRACKETS = /^\[|\]$/g;
|
|
11
|
+
const NUMERIC_IP = /^[\d.]+$/;
|
|
12
|
+
const HEX_IP = /^0x[0-9a-f]+$/i;
|
|
13
|
+
function isAllowedHref(href) {
|
|
14
|
+
const cleaned = href.replace(INVISIBLE_CHARS, '');
|
|
15
|
+
const trimmed = cleaned.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (CONTROL_CHARS.test(trimmed)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (DANGEROUS_HTML_CHARS.test(trimmed)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (ENCODED_COLON_SCHEME.test(trimmed)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (trimmed.startsWith('//')) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (trimmed.startsWith('\\')) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (trimmed.startsWith('/')) {
|
|
38
|
+
return trimmed[1] !== '\\';
|
|
39
|
+
}
|
|
40
|
+
if (trimmed.startsWith('#')) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(trimmed);
|
|
48
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (url.protocol === 'https:') {
|
|
52
|
+
if (!trimmed.startsWith('https://')) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!url.hostname) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (url.username || url.password) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (isInternalHost(url.hostname)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (url.protocol === 'mailto:') {
|
|
66
|
+
if (url.search) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (!url.pathname) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Synthetic origin used to resolve relative paths via the WHATWG URL parser.
|
|
79
|
+
// If the resolved origin still equals this sentinel, the path stays on-origin.
|
|
80
|
+
const SSR_SENTINEL_ORIGIN = 'https://internal.invalid';
|
|
81
|
+
function isHrefAllowedByMode(href, mode) {
|
|
82
|
+
switch (mode) {
|
|
83
|
+
case 'none':
|
|
84
|
+
return false;
|
|
85
|
+
case 'all':
|
|
86
|
+
return true;
|
|
87
|
+
case 'internal':
|
|
88
|
+
if (href.startsWith('https://wise.com/')) return true;
|
|
89
|
+
if (href.startsWith('/')) {
|
|
90
|
+
try {
|
|
91
|
+
return new URL(href, SSR_SENTINEL_ORIGIN).origin === SSR_SENTINEL_ORIGIN;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isInternalHost(hostname) {
|
|
100
|
+
const stripped = hostname.replace(IPV6_BRACKETS, '');
|
|
101
|
+
const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;
|
|
102
|
+
if (NUMERIC_IP.test(host)) return true;
|
|
103
|
+
if (HEX_IP.test(host)) return true;
|
|
104
|
+
if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;
|
|
105
|
+
if (host === 'localhost') return true;
|
|
106
|
+
if (host.endsWith('.internal')) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { isAllowedHref, isHrefAllowedByMode };
|
|
111
|
+
//# sourceMappingURL=sanitise.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitise.mjs","sources":["../../../src/Markup/utils/sanitise.ts"],"sourcesContent":["import { ALLOWED_PROTOCOLS } from '../constants';\n\nconst INVISIBLE_CHARS = /[\\u200b\\u200c\\u200d\\ufeff\\u00ad\\u2060\\u180e]/g;\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/;\nconst DANGEROUS_HTML_CHARS = /[<>\"']/;\nconst DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\\s*:/i;\n// eslint-disable-next-line regexp/no-useless-non-capturing-group\nconst ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;\nconst IPV6_BRACKETS = /^\\[|\\]$/g;\nconst NUMERIC_IP = /^[\\d.]+$/;\nconst HEX_IP = /^0x[0-9a-f]+$/i;\n\nexport function isAllowedHref(href: string): boolean {\n const cleaned = href.replace(INVISIBLE_CHARS, '');\n const trimmed = cleaned.trim();\n\n if (!trimmed) {\n return false;\n }\n\n if (CONTROL_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_HTML_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (ENCODED_COLON_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith('//')) {\n return false;\n }\n\n if (trimmed.startsWith('\\\\')) {\n return false;\n }\n\n if (trimmed.startsWith('/')) {\n return trimmed[1] !== '\\\\';\n }\n\n if (trimmed.startsWith('#')) {\n return true;\n }\n\n if (trimmed.startsWith('./') || trimmed.startsWith('../')) {\n return false;\n }\n\n try {\n const url = new URL(trimmed);\n\n if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {\n return false;\n }\n\n if (url.protocol === 'https:') {\n if (!trimmed.startsWith('https://')) {\n return false;\n }\n\n if (!url.hostname) {\n return false;\n }\n\n if (url.username || url.password) {\n return false;\n }\n\n if (isInternalHost(url.hostname)) {\n return false;\n }\n }\n\n if (url.protocol === 'mailto:') {\n if (url.search) {\n return false;\n }\n\n if (!url.pathname) {\n return false;\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport type AllowLinksMode = 'internal' | 'all' | 'none';\n\n// Synthetic origin used to resolve relative paths via the WHATWG URL parser.\n// If the resolved origin still equals this sentinel, the path stays on-origin.\nconst SSR_SENTINEL_ORIGIN = 'https://internal.invalid';\n\nexport function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean {\n switch (mode) {\n case 'none':\n return false;\n case 'all':\n return true;\n case 'internal':\n if (href.startsWith('https://wise.com/')) return true;\n if (href.startsWith('/')) {\n try {\n return new URL(href, SSR_SENTINEL_ORIGIN).origin === SSR_SENTINEL_ORIGIN;\n } catch {\n return false;\n }\n }\n return false;\n }\n}\n\nfunction isInternalHost(hostname: string): boolean {\n const stripped = hostname.replace(IPV6_BRACKETS, '');\n const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;\n if (NUMERIC_IP.test(host)) return true;\n if (HEX_IP.test(host)) return true;\n if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;\n if (host === 'localhost') return true;\n if (host.endsWith('.internal')) return true;\n return false;\n}\n"],"names":["INVISIBLE_CHARS","CONTROL_CHARS","DANGEROUS_HTML_CHARS","DANGEROUS_SCHEME","ENCODED_COLON_SCHEME","IPV6_BRACKETS","NUMERIC_IP","HEX_IP","isAllowedHref","href","cleaned","replace","trimmed","trim","test","startsWith","url","URL","ALLOWED_PROTOCOLS","includes","protocol","hostname","username","password","isInternalHost","search","pathname","SSR_SENTINEL_ORIGIN","isHrefAllowedByMode","mode","origin","stripped","host","endsWith","slice"],"mappings":"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"}
|
package/build/i18n/en.json
CHANGED
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
"neptune.DateLookup.goTo20YearView": "Go to 20 year view",
|
|
13
13
|
"neptune.DateLookup.month": "month",
|
|
14
14
|
"neptune.DateLookup.next": "next",
|
|
15
|
-
"neptune.DateLookup.notAvailable": "not available",
|
|
16
|
-
"neptune.DateLookup.outOfRangeAfter": "Please select a date on or after {min}.",
|
|
17
|
-
"neptune.DateLookup.outOfRangeBefore": "Please select a date on or before {max}.",
|
|
18
|
-
"neptune.DateLookup.outOfRangeBetween": "Please select a date between {min} and {max}.",
|
|
19
15
|
"neptune.DateLookup.previous": "previous",
|
|
20
16
|
"neptune.DateLookup.selected": "selected",
|
|
21
17
|
"neptune.DateLookup.twentyYears": "20 years",
|
package/build/i18n/en.json.js
CHANGED
|
@@ -16,10 +16,6 @@ var en = {
|
|
|
16
16
|
"neptune.DateLookup.goTo20YearView": "Go to 20 year view",
|
|
17
17
|
"neptune.DateLookup.month": "month",
|
|
18
18
|
"neptune.DateLookup.next": "next",
|
|
19
|
-
"neptune.DateLookup.notAvailable": "not available",
|
|
20
|
-
"neptune.DateLookup.outOfRangeAfter": "Please select a date on or after {min}.",
|
|
21
|
-
"neptune.DateLookup.outOfRangeBefore": "Please select a date on or before {max}.",
|
|
22
|
-
"neptune.DateLookup.outOfRangeBetween": "Please select a date between {min} and {max}.",
|
|
23
19
|
"neptune.DateLookup.previous": "previous",
|
|
24
20
|
"neptune.DateLookup.selected": "selected",
|
|
25
21
|
"neptune.DateLookup.twentyYears": "20 years",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/build/i18n/en.json.mjs
CHANGED
|
@@ -12,10 +12,6 @@ var en = {
|
|
|
12
12
|
"neptune.DateLookup.goTo20YearView": "Go to 20 year view",
|
|
13
13
|
"neptune.DateLookup.month": "month",
|
|
14
14
|
"neptune.DateLookup.next": "next",
|
|
15
|
-
"neptune.DateLookup.notAvailable": "not available",
|
|
16
|
-
"neptune.DateLookup.outOfRangeAfter": "Please select a date on or after {min}.",
|
|
17
|
-
"neptune.DateLookup.outOfRangeBefore": "Please select a date on or before {max}.",
|
|
18
|
-
"neptune.DateLookup.outOfRangeBetween": "Please select a date between {min} and {max}.",
|
|
19
15
|
"neptune.DateLookup.previous": "previous",
|
|
20
16
|
"neptune.DateLookup.selected": "selected",
|
|
21
17
|
"neptune.DateLookup.twentyYears": "20 years",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/build/index.js
CHANGED
|
@@ -15,7 +15,6 @@ var BadgeAssets = require('./_deprecated/Badge/BadgeAssets.js');
|
|
|
15
15
|
var Body = require('./Body/Body.js');
|
|
16
16
|
var Button_resolver = require('./Button/Button.resolver.js');
|
|
17
17
|
var IconButton = require('./IconButton/IconButton.js');
|
|
18
|
-
var Calendar = require('./Calendar/Calendar.js');
|
|
19
18
|
var Carousel = require('./Carousel/Carousel.js');
|
|
20
19
|
var Card = require('./_deprecated/Card/Card.js');
|
|
21
20
|
var Checkbox = require('./Checkbox/Checkbox.js');
|
|
@@ -32,6 +31,7 @@ var BaseCard = require('./common/BaseCard/BaseCard.js');
|
|
|
32
31
|
var CriticalCommsBanner = require('./_deprecated/CriticalBanner/CriticalCommsBanner.js');
|
|
33
32
|
var DateInput = require('./DateInput/DateInput.js');
|
|
34
33
|
var DateLookup = require('./DateLookup/DateLookup.js');
|
|
34
|
+
var Calendar = require('./Calendar/Calendar.js');
|
|
35
35
|
var Decision = require('./Decision/Decision.js');
|
|
36
36
|
var DefinitionList = require('./DefinitionList/DefinitionList.js');
|
|
37
37
|
var Dimmer = require('./Dimmer/Dimmer.js');
|
|
@@ -76,6 +76,7 @@ var LegacyListItem = require('./_deprecated/LegacyListItem/LegacyListItem.js');
|
|
|
76
76
|
var Loader = require('./Loader/Loader.js');
|
|
77
77
|
var Logo = require('./Logo/Logo.js');
|
|
78
78
|
var Markdown = require('./Markdown/Markdown.js');
|
|
79
|
+
var Markup = require('./Markup/Markup.js');
|
|
79
80
|
var Modal = require('./Modal/Modal.js');
|
|
80
81
|
var Money = require('./Money/Money.js');
|
|
81
82
|
var MoneyInput = require('./MoneyInput/MoneyInput.js');
|
|
@@ -177,7 +178,6 @@ exports.BadgeAssets = BadgeAssets.default;
|
|
|
177
178
|
exports.Body = Body.default;
|
|
178
179
|
exports.Button = Button_resolver.default;
|
|
179
180
|
exports.IconButton = IconButton.default;
|
|
180
|
-
exports.Calendar = Calendar.default;
|
|
181
181
|
exports.Carousel = Carousel.default;
|
|
182
182
|
exports.Card = Card.default;
|
|
183
183
|
exports.Checkbox = Checkbox.default;
|
|
@@ -194,6 +194,7 @@ exports.BaseCard = BaseCard.default;
|
|
|
194
194
|
exports.CriticalCommsBanner = CriticalCommsBanner.default;
|
|
195
195
|
exports.DateInput = DateInput.default;
|
|
196
196
|
exports.DateLookup = DateLookup.default;
|
|
197
|
+
exports.Calendar = Calendar.default;
|
|
197
198
|
exports.Decision = Decision.default;
|
|
198
199
|
Object.defineProperty(exports, "DecisionPresentation", {
|
|
199
200
|
enumerable: true,
|
|
@@ -244,6 +245,7 @@ Object.defineProperty(exports, "LogoType", {
|
|
|
244
245
|
get: function () { return Logo.LogoType; }
|
|
245
246
|
});
|
|
246
247
|
exports.Markdown = Markdown.default;
|
|
248
|
+
exports.Markup = Markup.Markup;
|
|
247
249
|
exports.Modal = Modal.default;
|
|
248
250
|
exports.Money = Money.default;
|
|
249
251
|
exports.MoneyInput = MoneyInput.default;
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|