ngwr 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/color-picker/styles/_index.scss +43 -5
- package/fesm2022/ngwr-alert.mjs +5 -5
- package/fesm2022/ngwr-alert.mjs.map +1 -1
- package/fesm2022/ngwr-back-top.mjs +11 -1
- package/fesm2022/ngwr-back-top.mjs.map +1 -1
- package/fesm2022/ngwr-blur-text.mjs +13 -2
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-border-glow.mjs +12 -3
- package/fesm2022/ngwr-border-glow.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +14 -3
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-button.mjs +4 -4
- package/fesm2022/ngwr-button.mjs.map +1 -1
- package/fesm2022/ngwr-calendar-testing.mjs +15 -0
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-calendar.mjs +51 -5
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-circular-text.mjs +20 -1
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-collapse.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker-testing.mjs +23 -7
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker.mjs +89 -10
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-command-palette.mjs +27 -3
- package/fesm2022/ngwr-command-palette.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +13 -3
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-adapters-fns.mjs +101 -33
- package/fesm2022/ngwr-date-adapters-fns.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker-testing.mjs +10 -2
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +103 -13
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/ngwr-date.mjs +480 -79
- package/fesm2022/ngwr-date.mjs.map +1 -1
- package/fesm2022/ngwr-dialog.mjs +22 -16
- package/fesm2022/ngwr-dialog.mjs.map +1 -1
- package/fesm2022/ngwr-directives.mjs +5 -2
- package/fesm2022/ngwr-directives.mjs.map +1 -1
- package/fesm2022/ngwr-drag-drop.mjs +30 -2
- package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
- package/fesm2022/ngwr-drawer.mjs +24 -18
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +8 -3
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar.mjs +1 -1
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-file-upload.mjs +52 -12
- package/fesm2022/ngwr-file-upload.mjs.map +1 -1
- package/fesm2022/ngwr-fuzzy-text.mjs +21 -4
- package/fesm2022/ngwr-fuzzy-text.mjs.map +1 -1
- package/fesm2022/ngwr-gradient-text.mjs +9 -2
- package/fesm2022/ngwr-gradient-text.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +63 -1
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +28 -1
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +150 -22
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-icon-adapters-feather.mjs +11 -0
- package/fesm2022/ngwr-icon-adapters-feather.mjs.map +1 -1
- package/fesm2022/ngwr-icon-adapters-lucide.mjs +22 -2
- package/fesm2022/ngwr-icon-adapters-lucide.mjs.map +1 -1
- package/fesm2022/ngwr-icon.mjs +474 -37
- package/fesm2022/ngwr-icon.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +3 -3
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-input-otp.mjs +21 -3
- package/fesm2022/ngwr-input-otp.mjs.map +1 -1
- package/fesm2022/ngwr-input.mjs +14 -7
- package/fesm2022/ngwr-input.mjs.map +1 -1
- package/fesm2022/ngwr-knob-testing.mjs.map +1 -1
- package/fesm2022/ngwr-knob.mjs +20 -2
- package/fesm2022/ngwr-knob.mjs.map +1 -1
- package/fesm2022/ngwr-loading-bar-router.mjs +73 -0
- package/fesm2022/ngwr-loading-bar-router.mjs.map +1 -0
- package/fesm2022/ngwr-loading-bar.mjs +21 -32
- package/fesm2022/ngwr-loading-bar.mjs.map +1 -1
- package/fesm2022/ngwr-markdown.mjs +63 -11
- package/fesm2022/ngwr-markdown.mjs.map +1 -1
- package/fesm2022/ngwr-mention.mjs +104 -14
- package/fesm2022/ngwr-mention.mjs.map +1 -1
- package/fesm2022/ngwr-overlay.mjs +36 -7
- package/fesm2022/ngwr-overlay.mjs.map +1 -1
- package/fesm2022/ngwr-pagination-testing.mjs +11 -10
- package/fesm2022/ngwr-pagination-testing.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +65 -30
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-popconfirm.mjs +3 -2
- package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +2 -1
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-qr.mjs +38 -1
- package/fesm2022/ngwr-qr.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +93 -34
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-shiny-text.mjs +10 -3
- package/fesm2022/ngwr-shiny-text.mjs.map +1 -1
- package/fesm2022/ngwr-splitter-testing.mjs.map +1 -1
- package/fesm2022/ngwr-squircle.mjs +2 -1
- package/fesm2022/ngwr-squircle.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +35 -4
- package/fesm2022/ngwr-statistic.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +35 -12
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs-router.mjs +158 -0
- package/fesm2022/ngwr-tabs-router.mjs.map +1 -0
- package/fesm2022/ngwr-tabs-testing.mjs +13 -11
- package/fesm2022/ngwr-tabs-testing.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +104 -12
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-textarea.mjs +20 -1
- package/fesm2022/ngwr-textarea.mjs.map +1 -1
- package/fesm2022/ngwr-tour.mjs +1 -1
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-transfer.mjs +2 -2
- package/fesm2022/ngwr-transfer.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +18 -2
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-utils.mjs +143 -1
- package/fesm2022/ngwr-utils.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-window-testing.mjs +10 -2
- package/fesm2022/ngwr-window-testing.mjs.map +1 -1
- package/fesm2022/ngwr-window.mjs +4 -4
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/i18n/en.json +33 -6
- package/i18n/ru.json +33 -6
- package/llms-full.txt +20 -11
- package/llms.txt +47 -16
- package/mcp/catalog.js +3 -3
- package/mcp/providers.js +11 -0
- package/package.json +11 -3
- package/schematics/migrations/v13/index.js +101 -0
- package/schematics/migrations/v14/index.js +422 -0
- package/schematics/migrations.json +10 -0
- package/schematics/page/templates.js +1 -1
- package/schematics/use/symbol-map.json +1 -0
- package/skills/ngwr/SKILL.md +4 -1
- package/skills/ngwr/references/catalog.md +2 -0
- package/skills/ngwr/references/setup.md +4 -0
- package/stepper/styles/_index.scss +19 -1
- package/textarea/styles/_index.scss +14 -6
- package/toast/styles/_index.scss +12 -4
- package/types/ngwr-alert.d.ts +3 -3
- package/types/ngwr-blur-text.d.ts +8 -0
- package/types/ngwr-breadcrumbs.d.ts +1 -0
- package/types/ngwr-button.d.ts +2 -2
- package/types/ngwr-calendar-testing.d.ts +21 -4
- package/types/ngwr-calendar.d.ts +37 -0
- package/types/ngwr-circular-text.d.ts +9 -0
- package/types/ngwr-collapse.d.ts +19 -15
- package/types/ngwr-color-picker-testing.d.ts +10 -4
- package/types/ngwr-color-picker.d.ts +70 -2
- package/types/ngwr-command-palette.d.ts +14 -0
- package/types/ngwr-context-menu.d.ts +1 -1
- package/types/ngwr-date-adapters-fns.d.ts +71 -29
- package/types/ngwr-date-picker-testing.d.ts +206 -185
- package/types/ngwr-date-picker.d.ts +7 -0
- package/types/ngwr-date.d.ts +143 -13
- package/types/ngwr-drag-drop.d.ts +19 -0
- package/types/ngwr-dropdown.d.ts +1 -1
- package/types/ngwr-event-calendar-testing.d.ts +12 -9
- package/types/ngwr-file-upload.d.ts +31 -3
- package/types/ngwr-fuzzy-text.d.ts +5 -0
- package/types/ngwr-i18n.d.ts +89 -7
- package/types/ngwr-icon-adapters-feather.d.ts +11 -0
- package/types/ngwr-icon.d.ts +42 -1
- package/types/ngwr-input-number.d.ts +1 -1
- package/types/ngwr-input-otp.d.ts +3 -0
- package/types/ngwr-input.d.ts +11 -4
- package/types/ngwr-knob-testing.d.ts +8 -4
- package/types/ngwr-knob.d.ts +18 -0
- package/types/ngwr-loading-bar-router.d.ts +35 -0
- package/types/ngwr-loading-bar.d.ts +19 -9
- package/types/ngwr-mention.d.ts +13 -0
- package/types/ngwr-pagination-testing.d.ts +11 -10
- package/types/ngwr-pagination.d.ts +39 -10
- package/types/ngwr-popconfirm.d.ts +1 -1
- package/types/ngwr-popover.d.ts +1 -1
- package/types/ngwr-select.d.ts +65 -10
- package/types/ngwr-shiny-text.d.ts +1 -0
- package/types/ngwr-splitter-testing.d.ts +9 -4
- package/types/ngwr-squircle.d.ts +1 -1
- package/types/ngwr-statistic.d.ts +22 -2
- package/types/ngwr-table.d.ts +24 -5
- package/types/ngwr-tabs-router.d.ts +67 -0
- package/types/ngwr-tabs-testing.d.ts +12 -10
- package/types/ngwr-tabs.d.ts +103 -6
- package/types/ngwr-textarea.d.ts +9 -0
- package/types/ngwr-tree.d.ts +7 -0
- package/types/ngwr-utils.d.ts +79 -1
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-window-testing.d.ts +11 -3
- package/types/ngwr-window.d.ts +11 -4
- package/window/styles/_index.scss +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-icon-adapters-feather.mjs","sources":["../../../projects/lib/icon/adapters/feather/adapter.ts","../../../projects/lib/icon/adapters/feather/public-api.ts","../../../projects/lib/icon/adapters/feather/ngwr-icon-adapters-feather.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport type { WrIconDef } from 'ngwr/icon';\n\n/**\n * Default SVG attributes Feather applies to every icon. Kept identical\n * to what `feather.replace()` emits in the browser so visual parity\n * with upstream is preserved.\n */\nconst SVG_ATTRS = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n 'width=\"24\"',\n 'height=\"24\"',\n 'viewBox=\"0 0 24 24\"',\n 'fill=\"none\"',\n 'stroke=\"currentColor\"',\n 'stroke-width=\"2\"',\n 'stroke-linecap=\"round\"',\n 'stroke-linejoin=\"round\"',\n 'class=\"wr-icon__svg feather\"',\n].join(' ');\n\n/**\n * Wrap a single Feather icon (inner SVG markup) in a {@link WrIconDef}\n * envelope. Pair with the upstream `icons.json` map.\n *\n * @example\n * ```ts\n * import { feather } from 'ngwr/icon/adapters/feather';\n * // feather-icons ships an inner-SVG-only map under dist/icons.json:\n * // { \"plus\": \"<line .../><line .../>\", \"trash\": \"...\", ... }\n * import featherIcons from 'feather-icons/dist/icons.json';\n *\n * provideWrIcons([feather('plus', featherIcons.plus)]);\n * ```\n */\nfunction feather(name: string, innerSvg: string): WrIconDef {\n return { name, data: `<svg ${SVG_ATTRS}>${innerSvg}</svg>` };\n}\n\n/**\n * Bulk variant — pass a `{ alias: innerSvg }` object. Aliases (the\n * keys) become the registry names so you can use shorter or\n * project-specific names.\n *\n * @example\n * ```ts\n * import { featherIcons } from 'ngwr/icon/adapters/feather';\n * import featherSource from 'feather-icons/dist/icons.json';\n *\n * provideWrIcons(featherIcons({\n * plus: featherSource.plus,\n * trash: featherSource.trash,\n * x: featherSource.x,\n * }));\n * ```\n */\nfunction featherIcons(icons: Record<string, string>): WrIconDef[] {\n return Object.entries(icons).map(([name, innerSvg]) => feather(name, innerSvg));\n}\n\nexport { feather, featherIcons };\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nexport { feather, featherIcons } from './adapter';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"AAAA;;;;;AAKG;AAIH;;;;AAIG;AACH,MAAM,SAAS,GAAG;IAChB,oCAAoC;IACpC,YAAY;IACZ,aAAa;IACb,qBAAqB;IACrB,aAAa;IACb,uBAAuB;IACvB,kBAAkB;IAClB,wBAAwB;IACxB,yBAAyB;IACzB,8BAA8B;AAC/B,CAAA,CAAC,IAAI,CAAC,GAAG,CAAC;AAEX
|
|
1
|
+
{"version":3,"file":"ngwr-icon-adapters-feather.mjs","sources":["../../../projects/lib/icon/adapters/feather/adapter.ts","../../../projects/lib/icon/adapters/feather/public-api.ts","../../../projects/lib/icon/adapters/feather/ngwr-icon-adapters-feather.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport type { WrIconDef } from 'ngwr/icon';\n\n/**\n * Default SVG attributes Feather applies to every icon. Kept identical\n * to what `feather.replace()` emits in the browser so visual parity\n * with upstream is preserved.\n */\nconst SVG_ATTRS = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n 'width=\"24\"',\n 'height=\"24\"',\n 'viewBox=\"0 0 24 24\"',\n 'fill=\"none\"',\n 'stroke=\"currentColor\"',\n 'stroke-width=\"2\"',\n 'stroke-linecap=\"round\"',\n 'stroke-linejoin=\"round\"',\n 'class=\"wr-icon__svg feather\"',\n].join(' ');\n\n/**\n * Wrap a single Feather icon (inner SVG markup) in a {@link WrIconDef}\n * envelope. Pair with the upstream `icons.json` map.\n *\n * @example\n * ```ts\n * import { feather } from 'ngwr/icon/adapters/feather';\n * // feather-icons ships an inner-SVG-only map under dist/icons.json:\n * // { \"plus\": \"<line .../><line .../>\", \"trash\": \"...\", ... }\n * import featherIcons from 'feather-icons/dist/icons.json';\n *\n * provideWrIcons([feather('plus', featherIcons.plus)]);\n * ```\n *\n * `innerSvg` is **markup**, not text — that is the shape upstream ships and the\n * reason this adapter exists — so it is interpolated into the envelope\n * verbatim. There is no escaping to add here: escaped markup is not markup.\n * A `</svg>` in that input therefore closes the envelope early, and everything\n * after it is a sibling of the icon rather than a child. Nothing executes\n * regardless, because `<wr-icon>` never assigns the result to `innerHTML` — it\n * rebuilds the first `<svg>` root from an allowlist and drops the rest. Feed\n * this a map fetched at runtime and the sanitizer is what stands between it and\n * the app's origin; feed it the `icons.json` from your own node_modules and\n * neither matters.\n */\nfunction feather(name: string, innerSvg: string): WrIconDef {\n return { name, data: `<svg ${SVG_ATTRS}>${innerSvg}</svg>` };\n}\n\n/**\n * Bulk variant — pass a `{ alias: innerSvg }` object. Aliases (the\n * keys) become the registry names so you can use shorter or\n * project-specific names.\n *\n * @example\n * ```ts\n * import { featherIcons } from 'ngwr/icon/adapters/feather';\n * import featherSource from 'feather-icons/dist/icons.json';\n *\n * provideWrIcons(featherIcons({\n * plus: featherSource.plus,\n * trash: featherSource.trash,\n * x: featherSource.x,\n * }));\n * ```\n */\nfunction featherIcons(icons: Record<string, string>): WrIconDef[] {\n return Object.entries(icons).map(([name, innerSvg]) => feather(name, innerSvg));\n}\n\nexport { feather, featherIcons };\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nexport { feather, featherIcons } from './adapter';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"AAAA;;;;;AAKG;AAIH;;;;AAIG;AACH,MAAM,SAAS,GAAG;IAChB,oCAAoC;IACpC,YAAY;IACZ,aAAa;IACb,qBAAqB;IACrB,aAAa;IACb,uBAAuB;IACvB,kBAAkB;IAClB,wBAAwB;IACxB,yBAAyB;IACzB,8BAA8B;AAC/B,CAAA,CAAC,IAAI,CAAC,GAAG,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAA;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA,KAAA,EAAQ,SAAS,CAAA,CAAA,EAAI,QAAQ,CAAA,MAAA,CAAQ,EAAE;AAC9D;AAEA;;;;;;;;;;;;;;;;AAgBG;AACH,SAAS,YAAY,CAAC,KAA6B,EAAA;IACjD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACjF;;AC3EA;;;;;AAKG;;ACLH;;AAEG;;;;"}
|
|
@@ -64,10 +64,30 @@ function lucide(name, children) {
|
|
|
64
64
|
function lucideIcons(icons) {
|
|
65
65
|
return Object.entries(icons).map(([name, node]) => lucide(name, node));
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* A tag or attribute name that can only ever be a name.
|
|
69
|
+
*
|
|
70
|
+
* `renderChild` builds markup by concatenation, so a name carrying a quote, a
|
|
71
|
+
* space or an `=` does not become a strangely-named attribute — it closes the
|
|
72
|
+
* one being written and opens whatever comes next, which is how
|
|
73
|
+
* `{ 'href': 'x" onerror="…' }`'s sibling, a hostile KEY, injects a handler
|
|
74
|
+
* through an API whose signature says `Record<string, string | number>`.
|
|
75
|
+
* Escaping cannot help a name: `onerror` is a perfectly valid name and the only
|
|
76
|
+
* honest serialisation of it is `onerror="…"`. So names are constrained and
|
|
77
|
+
* values are escaped, and it is {@link sanitizeIcon} at the render sink that
|
|
78
|
+
* refuses `onerror` itself.
|
|
79
|
+
*/
|
|
80
|
+
const NAME_RE = /^[A-Za-z_][\w.:-]*$/;
|
|
81
|
+
/** The four characters that can leave an attribute value or a tag body. */
|
|
82
|
+
function escapeAttribute(value) {
|
|
83
|
+
return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
84
|
+
}
|
|
67
85
|
function renderChild([tag, attrs]) {
|
|
86
|
+
if (!NAME_RE.test(tag))
|
|
87
|
+
return '';
|
|
68
88
|
const rendered = Object.entries(attrs)
|
|
69
|
-
.filter((entry) => entry[1] !== undefined)
|
|
70
|
-
.map(([key, value]) => `${key}="${value}"`)
|
|
89
|
+
.filter((entry) => entry[1] !== undefined && NAME_RE.test(entry[0]))
|
|
90
|
+
.map(([key, value]) => `${key}="${escapeAttribute(value)}"`)
|
|
71
91
|
.join(' ');
|
|
72
92
|
return `<${tag} ${rendered}/>`;
|
|
73
93
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-icon-adapters-lucide.mjs","sources":["../../../projects/lib/icon/adapters/lucide/adapter.ts","../../../projects/lib/icon/adapters/lucide/public-api.ts","../../../projects/lib/icon/adapters/lucide/ngwr-icon-adapters-lucide.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport type { WrIconDef } from 'ngwr/icon';\n\n/**\n * Shape of a `lucide` icon export — an array of `[tag, attrs]` tuples\n * representing the inner SVG children. The outer `<svg>` wrapper is\n * added by the adapter when converting to a {@link WrIconDef}.\n */\ntype LucideIconNode = readonly (readonly [string, Record<string, string | number | undefined>])[];\n\n/**\n * Default SVG attributes Lucide applies to every icon. Kept identical\n * to what the `lucide` package's `createElement` would emit so visual\n * parity with upstream is preserved.\n */\nconst SVG_ATTRS = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n 'width=\"24\"',\n 'height=\"24\"',\n 'viewBox=\"0 0 24 24\"',\n 'fill=\"none\"',\n 'stroke=\"currentColor\"',\n 'stroke-width=\"2\"',\n 'stroke-linecap=\"round\"',\n 'stroke-linejoin=\"round\"',\n 'class=\"wr-icon__svg lucide\"',\n].join(' ');\n\n/**\n * Wrap a single Lucide icon in the {@link WrIconDef} envelope. Use this\n * when you need fine-grained control over the registered name, or when\n * the bulk {@link lucideIcons} helper isn't a good fit.\n *\n * @example\n * ```ts\n * import { Plus } from 'lucide';\n * import { lucide } from 'ngwr/icon/adapters/lucide';\n *\n * provideWrIcons([lucide('plus', Plus)]);\n * ```\n */\nfunction lucide(name: string, children: LucideIconNode): WrIconDef {\n const inner = children.map(renderChild).join('');\n return { name, data: `<svg ${SVG_ATTRS}>${inner}</svg>` };\n}\n\n/**\n * Wrap a bag of Lucide icons in one shot. Keys become the registered\n * names **verbatim** — exactly what you pass to `<wr-icon name>` — matching\n * the singular `lucide()` helper and the other adapters. Kebab-case by\n * convention, so quote multi-word keys (`'chevron-down'`). Values are the\n * upstream IconNode tuples.\n *\n * Tree-shaking: every `Plus`, `Trash`, … import lives in *your* file\n * — ngwr ships only this wrapper, not the icon data. Unused icons in\n * `lucide` itself get dropped by the bundler.\n *\n * @example\n * ```ts\n * import { Plus, Trash, ChevronDown } from 'lucide';\n * import { lucideIcons } from 'ngwr/icon/adapters/lucide';\n *\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideWrIcons(lucideIcons({ plus: Plus, trash: Trash, 'chevron-down': ChevronDown })),\n * ],\n * });\n * ```\n */\nfunction lucideIcons(icons: Record<string, LucideIconNode>): WrIconDef[] {\n return Object.entries(icons).map(([name, node]) => lucide(name, node));\n}\n\nfunction renderChild([tag, attrs]: readonly [string, Record<string, string | number | undefined>]): string {\n const rendered = Object.entries(attrs)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined)\n .map(([key, value]) => `${key}=\"${value}\"`)\n .join(' ');\n return `<${tag} ${rendered}/>`;\n}\n\nexport { lucide, lucideIcons, type LucideIconNode };\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nexport { lucide, lucideIcons, type LucideIconNode } from './adapter';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"AAAA;;;;;AAKG;AAWH;;;;AAIG;AACH,MAAM,SAAS,GAAG;IAChB,oCAAoC;IACpC,YAAY;IACZ,aAAa;IACb,qBAAqB;IACrB,aAAa;IACb,uBAAuB;IACvB,kBAAkB;IAClB,wBAAwB;IACxB,yBAAyB;IACzB,6BAA6B;AAC9B,CAAA,CAAC,IAAI,CAAC,GAAG,CAAC;AAEX;;;;;;;;;;;;AAYG;AACH,SAAS,MAAM,CAAC,IAAY,EAAE,QAAwB,EAAA;AACpD,IAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;IAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA,KAAA,EAAQ,SAAS,CAAA,CAAA,EAAI,KAAK,CAAA,MAAA,CAAQ,EAAE;AAC3D;AAEA;;;;;;;;;;;;;;;;;;;;;;AAsBG;AACH,SAAS,WAAW,CAAC,KAAqC,EAAA;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACxE;AAEA,SAAS,WAAW,CAAC,CAAC,GAAG,EAAE,KAAK,CAAiE,EAAA;AAC/F,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;
|
|
1
|
+
{"version":3,"file":"ngwr-icon-adapters-lucide.mjs","sources":["../../../projects/lib/icon/adapters/lucide/adapter.ts","../../../projects/lib/icon/adapters/lucide/public-api.ts","../../../projects/lib/icon/adapters/lucide/ngwr-icon-adapters-lucide.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport type { WrIconDef } from 'ngwr/icon';\n\n/**\n * Shape of a `lucide` icon export — an array of `[tag, attrs]` tuples\n * representing the inner SVG children. The outer `<svg>` wrapper is\n * added by the adapter when converting to a {@link WrIconDef}.\n */\ntype LucideIconNode = readonly (readonly [string, Record<string, string | number | undefined>])[];\n\n/**\n * Default SVG attributes Lucide applies to every icon. Kept identical\n * to what the `lucide` package's `createElement` would emit so visual\n * parity with upstream is preserved.\n */\nconst SVG_ATTRS = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n 'width=\"24\"',\n 'height=\"24\"',\n 'viewBox=\"0 0 24 24\"',\n 'fill=\"none\"',\n 'stroke=\"currentColor\"',\n 'stroke-width=\"2\"',\n 'stroke-linecap=\"round\"',\n 'stroke-linejoin=\"round\"',\n 'class=\"wr-icon__svg lucide\"',\n].join(' ');\n\n/**\n * Wrap a single Lucide icon in the {@link WrIconDef} envelope. Use this\n * when you need fine-grained control over the registered name, or when\n * the bulk {@link lucideIcons} helper isn't a good fit.\n *\n * @example\n * ```ts\n * import { Plus } from 'lucide';\n * import { lucide } from 'ngwr/icon/adapters/lucide';\n *\n * provideWrIcons([lucide('plus', Plus)]);\n * ```\n */\nfunction lucide(name: string, children: LucideIconNode): WrIconDef {\n const inner = children.map(renderChild).join('');\n return { name, data: `<svg ${SVG_ATTRS}>${inner}</svg>` };\n}\n\n/**\n * Wrap a bag of Lucide icons in one shot. Keys become the registered\n * names **verbatim** — exactly what you pass to `<wr-icon name>` — matching\n * the singular `lucide()` helper and the other adapters. Kebab-case by\n * convention, so quote multi-word keys (`'chevron-down'`). Values are the\n * upstream IconNode tuples.\n *\n * Tree-shaking: every `Plus`, `Trash`, … import lives in *your* file\n * — ngwr ships only this wrapper, not the icon data. Unused icons in\n * `lucide` itself get dropped by the bundler.\n *\n * @example\n * ```ts\n * import { Plus, Trash, ChevronDown } from 'lucide';\n * import { lucideIcons } from 'ngwr/icon/adapters/lucide';\n *\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideWrIcons(lucideIcons({ plus: Plus, trash: Trash, 'chevron-down': ChevronDown })),\n * ],\n * });\n * ```\n */\nfunction lucideIcons(icons: Record<string, LucideIconNode>): WrIconDef[] {\n return Object.entries(icons).map(([name, node]) => lucide(name, node));\n}\n\n/**\n * A tag or attribute name that can only ever be a name.\n *\n * `renderChild` builds markup by concatenation, so a name carrying a quote, a\n * space or an `=` does not become a strangely-named attribute — it closes the\n * one being written and opens whatever comes next, which is how\n * `{ 'href': 'x\" onerror=\"…' }`'s sibling, a hostile KEY, injects a handler\n * through an API whose signature says `Record<string, string | number>`.\n * Escaping cannot help a name: `onerror` is a perfectly valid name and the only\n * honest serialisation of it is `onerror=\"…\"`. So names are constrained and\n * values are escaped, and it is {@link sanitizeIcon} at the render sink that\n * refuses `onerror` itself.\n */\nconst NAME_RE = /^[A-Za-z_][\\w.:-]*$/;\n\n/** The four characters that can leave an attribute value or a tag body. */\nfunction escapeAttribute(value: string | number): string {\n return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n}\n\nfunction renderChild([tag, attrs]: readonly [string, Record<string, string | number | undefined>]): string {\n if (!NAME_RE.test(tag)) return '';\n\n const rendered = Object.entries(attrs)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined && NAME_RE.test(entry[0]))\n .map(([key, value]) => `${key}=\"${escapeAttribute(value)}\"`)\n .join(' ');\n return `<${tag} ${rendered}/>`;\n}\n\nexport { lucide, lucideIcons, type LucideIconNode };\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nexport { lucide, lucideIcons, type LucideIconNode } from './adapter';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"AAAA;;;;;AAKG;AAWH;;;;AAIG;AACH,MAAM,SAAS,GAAG;IAChB,oCAAoC;IACpC,YAAY;IACZ,aAAa;IACb,qBAAqB;IACrB,aAAa;IACb,uBAAuB;IACvB,kBAAkB;IAClB,wBAAwB;IACxB,yBAAyB;IACzB,6BAA6B;AAC9B,CAAA,CAAC,IAAI,CAAC,GAAG,CAAC;AAEX;;;;;;;;;;;;AAYG;AACH,SAAS,MAAM,CAAC,IAAY,EAAE,QAAwB,EAAA;AACpD,IAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;IAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA,KAAA,EAAQ,SAAS,CAAA,CAAA,EAAI,KAAK,CAAA,MAAA,CAAQ,EAAE;AAC3D;AAEA;;;;;;;;;;;;;;;;;;;;;;AAsBG;AACH,SAAS,WAAW,CAAC,KAAqC,EAAA;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACxE;AAEA;;;;;;;;;;;;AAYG;AACH,MAAM,OAAO,GAAG,qBAAqB;AAErC;AACA,SAAS,eAAe,CAAC,KAAsB,EAAA;AAC7C,IAAA,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;AACjH;AAEA,SAAS,WAAW,CAAC,CAAC,GAAG,EAAE,KAAK,CAAiE,EAAA;AAC/F,IAAA,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;AAAE,QAAA,OAAO,EAAE;AAEjC,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;SAClC,MAAM,CAAC,CAAC,KAAK,KAAyC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtG,SAAA,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAA,EAAG,GAAG,CAAA,EAAA,EAAK,eAAe,CAAC,KAAK,CAAC,GAAG;SAC1D,IAAI,CAAC,GAAG,CAAC;AACZ,IAAA,OAAO,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,EAAI,QAAQ,IAAI;AAChC;;AC3GA;;;;;AAKG;;ACLH;;AAEG;;;;"}
|
package/fesm2022/ngwr-icon.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DOCUMENT } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
3
|
import { InjectionToken, inject, Injectable, input, ElementRef, computed, effect, isDevMode, ViewEncapsulation, Component } from '@angular/core';
|
|
3
4
|
|
|
@@ -60,6 +61,402 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
|
|
|
60
61
|
type: Injectable
|
|
61
62
|
}] });
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @license
|
|
66
|
+
*
|
|
67
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
68
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
69
|
+
*/
|
|
70
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
71
|
+
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
72
|
+
/**
|
|
73
|
+
* SVG elements an icon may be built from.
|
|
74
|
+
*
|
|
75
|
+
* Derived from the real sets rather than guessed: across the 12 459 SVG files
|
|
76
|
+
* under `icon/_svg` (Lucide, Phosphor, Tabler, Heroicons), the whole of
|
|
77
|
+
* `feather-icons`' `icons.json` and every `lucide` IconNode, only eight
|
|
78
|
+
* elements ever appear — `svg`, `path`, `circle`, `line`, `rect`, `polyline`,
|
|
79
|
+
* `polygon`, `ellipse`. The rest of this list is headroom for the icons a
|
|
80
|
+
* consumer draws themselves: grouping, `<defs>` with gradients, a clip path, a
|
|
81
|
+
* mask, a `<title>`, real `<text>`.
|
|
82
|
+
*
|
|
83
|
+
* What is deliberately absent is the entire scripting surface:
|
|
84
|
+
*
|
|
85
|
+
* - `<script>` — obvious, and the one vector `innerHTML` already refused;
|
|
86
|
+
* - `<foreignObject>` — an XHTML escape hatch, so `<img onerror>` inside an
|
|
87
|
+
* otherwise well-formed icon;
|
|
88
|
+
* - `<image>` — its `onerror` fires on any unreachable `href`, which is the
|
|
89
|
+
* cheapest handler to land, and no real icon set uses it;
|
|
90
|
+
* - `<a>` — a `javascript:` href inside a glyph the user is invited to click;
|
|
91
|
+
* - `<animate>` / `<set>` / `<animateTransform>` / `<animateMotion>` — SMIL can
|
|
92
|
+
* retarget another element's `href` at a `javascript:` URL;
|
|
93
|
+
* - `<style>` — arbitrary CSS in the app's origin, including `url()` requests;
|
|
94
|
+
* - `<filter>` and the `fe*` primitives — `<feImage href>` is another external
|
|
95
|
+
* fetch, and no icon set in the corpus filters.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
const ELEMENTS = new Set([
|
|
100
|
+
'svg',
|
|
101
|
+
'g',
|
|
102
|
+
'defs',
|
|
103
|
+
'symbol',
|
|
104
|
+
'use',
|
|
105
|
+
'title',
|
|
106
|
+
'desc',
|
|
107
|
+
'path',
|
|
108
|
+
'circle',
|
|
109
|
+
'ellipse',
|
|
110
|
+
'line',
|
|
111
|
+
'polyline',
|
|
112
|
+
'polygon',
|
|
113
|
+
'rect',
|
|
114
|
+
'text',
|
|
115
|
+
'tspan',
|
|
116
|
+
'textPath',
|
|
117
|
+
'linearGradient',
|
|
118
|
+
'radialGradient',
|
|
119
|
+
'stop',
|
|
120
|
+
'clipPath',
|
|
121
|
+
'mask',
|
|
122
|
+
'pattern',
|
|
123
|
+
]);
|
|
124
|
+
/**
|
|
125
|
+
* Attributes an icon may carry, allowed on every element it is allowed on.
|
|
126
|
+
*
|
|
127
|
+
* An allowlist rather than a blocklist, so `onload`, `onerror`, `onclick` and
|
|
128
|
+
* every other handler — including ones no browser has shipped yet — are gone by
|
|
129
|
+
* construction rather than by enumeration.
|
|
130
|
+
*
|
|
131
|
+
* `style` is NOT here, and that is a decision rather than an oversight: not one
|
|
132
|
+
* icon in the corpus uses it, and a `style` attribute is arbitrary CSS in the
|
|
133
|
+
* app's origin. Anything a `style` would have done, a presentation attribute
|
|
134
|
+
* does.
|
|
135
|
+
*
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
const ATTRIBUTES = new Set([
|
|
139
|
+
// geometry
|
|
140
|
+
'viewBox',
|
|
141
|
+
'preserveAspectRatio',
|
|
142
|
+
'transform',
|
|
143
|
+
'transform-origin',
|
|
144
|
+
'd',
|
|
145
|
+
'pathLength',
|
|
146
|
+
'cx',
|
|
147
|
+
'cy',
|
|
148
|
+
'r',
|
|
149
|
+
'rx',
|
|
150
|
+
'ry',
|
|
151
|
+
'x',
|
|
152
|
+
'y',
|
|
153
|
+
'x1',
|
|
154
|
+
'y1',
|
|
155
|
+
'x2',
|
|
156
|
+
'y2',
|
|
157
|
+
'dx',
|
|
158
|
+
'dy',
|
|
159
|
+
'width',
|
|
160
|
+
'height',
|
|
161
|
+
'points',
|
|
162
|
+
// paint servers, clips and masks
|
|
163
|
+
'gradientUnits',
|
|
164
|
+
'gradientTransform',
|
|
165
|
+
'spreadMethod',
|
|
166
|
+
'offset',
|
|
167
|
+
'fx',
|
|
168
|
+
'fy',
|
|
169
|
+
'fr',
|
|
170
|
+
'patternUnits',
|
|
171
|
+
'patternContentUnits',
|
|
172
|
+
'patternTransform',
|
|
173
|
+
'maskUnits',
|
|
174
|
+
'maskContentUnits',
|
|
175
|
+
'clipPathUnits',
|
|
176
|
+
// presentation
|
|
177
|
+
'fill',
|
|
178
|
+
'fill-opacity',
|
|
179
|
+
'fill-rule',
|
|
180
|
+
'stroke',
|
|
181
|
+
'stroke-width',
|
|
182
|
+
'stroke-linecap',
|
|
183
|
+
'stroke-linejoin',
|
|
184
|
+
'stroke-miterlimit',
|
|
185
|
+
'stroke-dasharray',
|
|
186
|
+
'stroke-dashoffset',
|
|
187
|
+
'stroke-opacity',
|
|
188
|
+
'opacity',
|
|
189
|
+
'color',
|
|
190
|
+
'clip-path',
|
|
191
|
+
'clip-rule',
|
|
192
|
+
'mask',
|
|
193
|
+
'paint-order',
|
|
194
|
+
'vector-effect',
|
|
195
|
+
'shape-rendering',
|
|
196
|
+
'stop-color',
|
|
197
|
+
'stop-opacity',
|
|
198
|
+
'display',
|
|
199
|
+
'visibility',
|
|
200
|
+
'overflow',
|
|
201
|
+
'isolation',
|
|
202
|
+
'mix-blend-mode',
|
|
203
|
+
// text
|
|
204
|
+
'startOffset',
|
|
205
|
+
'textLength',
|
|
206
|
+
'lengthAdjust',
|
|
207
|
+
'text-anchor',
|
|
208
|
+
'dominant-baseline',
|
|
209
|
+
'alignment-baseline',
|
|
210
|
+
'baseline-shift',
|
|
211
|
+
'font-family',
|
|
212
|
+
'font-size',
|
|
213
|
+
'font-stretch',
|
|
214
|
+
'font-style',
|
|
215
|
+
'font-variant',
|
|
216
|
+
'font-weight',
|
|
217
|
+
'letter-spacing',
|
|
218
|
+
'word-spacing',
|
|
219
|
+
'writing-mode',
|
|
220
|
+
'direction',
|
|
221
|
+
// identity, accessibility and document plumbing
|
|
222
|
+
'id',
|
|
223
|
+
'class',
|
|
224
|
+
'role',
|
|
225
|
+
'aria-hidden',
|
|
226
|
+
'aria-label',
|
|
227
|
+
'aria-labelledby',
|
|
228
|
+
'aria-describedby',
|
|
229
|
+
'aria-roledescription',
|
|
230
|
+
'focusable',
|
|
231
|
+
'lang',
|
|
232
|
+
'version',
|
|
233
|
+
'baseProfile',
|
|
234
|
+
'xmlns',
|
|
235
|
+
'xmlns:xlink',
|
|
236
|
+
'xml:space',
|
|
237
|
+
'xml:lang',
|
|
238
|
+
// resolved against ONLY_FRAGMENT below, never free-form
|
|
239
|
+
'href',
|
|
240
|
+
'xlink:href',
|
|
241
|
+
]);
|
|
242
|
+
/**
|
|
243
|
+
* `href` may point at a fragment in the same document and nowhere else — that
|
|
244
|
+
* is the whole legitimate use inside an icon (`<use href="#glyph">`), and it
|
|
245
|
+
* excludes `javascript:`, `data:` and every remote fetch in one rule.
|
|
246
|
+
*/
|
|
247
|
+
const FRAGMENT_ONLY = new Set(['href', 'xlink:href']);
|
|
248
|
+
/** A same-document fragment reference: `#id`, and nothing that is not one. */
|
|
249
|
+
const FRAGMENT_RE = /^#[^\s]+$/;
|
|
250
|
+
/**
|
|
251
|
+
* `url(` inside a presentation attribute is a reference to a paint server, a
|
|
252
|
+
* clip path or a mask. Those are fragment references too; a remote one is at
|
|
253
|
+
* best a request the page did not ask for.
|
|
254
|
+
*/
|
|
255
|
+
const URL_FN_RE = /url\(/i;
|
|
256
|
+
const URL_FRAGMENT_RE = /^url\(\s*(['"]?)#[^\s'")]+\1\s*\)$/i;
|
|
257
|
+
/**
|
|
258
|
+
* Icons are shallow — the deepest in the corpus nests three levels. The cap is
|
|
259
|
+
* there so a hostile registration cannot recurse this function into a stack
|
|
260
|
+
* overflow; content below it is dropped, not truncated silently, because
|
|
261
|
+
* {@link WrIconStripReport} names what went.
|
|
262
|
+
*/
|
|
263
|
+
const MAX_DEPTH = 16;
|
|
264
|
+
/**
|
|
265
|
+
* Build a live `<svg>` element out of a registered icon's markup, keeping only
|
|
266
|
+
* what an icon legitimately needs.
|
|
267
|
+
*
|
|
268
|
+
* `<wr-icon>` used to assign `icon.data` to `innerHTML`, which executes an
|
|
269
|
+
* `onload` on the root, an `<image onerror>`, an `<img>` smuggled in after a
|
|
270
|
+
* closed `</svg>`, and anything inside a `<foreignObject>` — with the app's own
|
|
271
|
+
* origin, in a production build. The library already refuses exactly this next
|
|
272
|
+
* door: `<wr-markdown>` escapes raw HTML because its input is untrusted by
|
|
273
|
+
* construction and one `<img onerror>` is the whole cost of being wrong. Icon
|
|
274
|
+
* data arrives from `provideWrIcons()`, which a multi-tenant app or a fetched
|
|
275
|
+
* icon pack fills at runtime, so it is untrusted the same way.
|
|
276
|
+
*
|
|
277
|
+
* Three properties do the work, and none of them is a filter over known-bad
|
|
278
|
+
* strings:
|
|
279
|
+
*
|
|
280
|
+
* 1. **Parsed inert, never inserted.** The markup is parsed into a document
|
|
281
|
+
* with no browsing context, so nothing loads and nothing runs even before it
|
|
282
|
+
* is inspected.
|
|
283
|
+
* 2. **Only the first `<svg>` survives.** An icon IS an SVG element, so
|
|
284
|
+
* everything beside the root is dropped — which is what makes
|
|
285
|
+
* `</svg><img src=x onerror=…>` inert regardless of what the payload says,
|
|
286
|
+
* and it is the same shape as the `feather()` shell escape.
|
|
287
|
+
* 3. **Rebuilt, not filtered.** Every element and attribute that lands in the
|
|
288
|
+
* document is one this file created from an allowlist. Nothing is
|
|
289
|
+
* transferred from the parsed tree, so there is no attribute an unfamiliar
|
|
290
|
+
* payload can smuggle past a matcher.
|
|
291
|
+
*
|
|
292
|
+
* A refused element takes its subtree with it rather than having its children
|
|
293
|
+
* hoisted into its parent. Hoisting would preserve more of a hand-drawn icon
|
|
294
|
+
* wrapped in something unsupported, and it is exactly the kind of cleverness
|
|
295
|
+
* that makes "what does the output contain" depend on what the parser saw. The
|
|
296
|
+
* blunt rule is the reviewable one, and the dev-mode warning names what went.
|
|
297
|
+
*
|
|
298
|
+
* `DomSanitizer.sanitize(SecurityContext.HTML, …)` is not an option here, which
|
|
299
|
+
* is worth recording so it is not re-proposed: Angular's HTML allowlist
|
|
300
|
+
* (`VOID_ELEMENTS`, `BLOCK_ELEMENTS`, `INLINE_ELEMENTS`) contains no SVG
|
|
301
|
+
* element at all. Run against real Lucide, Feather and Heroicons icons it does
|
|
302
|
+
* not merely drop presentation attributes — it returns the empty string.
|
|
303
|
+
*
|
|
304
|
+
* @returns the rebuilt element, or `null` when the data holds no `<svg>` root.
|
|
305
|
+
* @internal
|
|
306
|
+
*/
|
|
307
|
+
function sanitizeIcon(doc, data, report) {
|
|
308
|
+
const source = parseInert(doc, data);
|
|
309
|
+
return source ? copy(doc, source, report, 0) : null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parse into a document that cannot run anything.
|
|
313
|
+
*
|
|
314
|
+
* `DOMParser` is the path every browser and the unit-test DOM take: it takes a
|
|
315
|
+
* plain string, so it is not a Trusted Types sink, and its result has no
|
|
316
|
+
* browsing context. It does not exist under SSR — `@angular/platform-server`
|
|
317
|
+
* renders into domino — so the server falls back to a detached `<template>`,
|
|
318
|
+
* whose content is a separate inert document for the same reason. That branch
|
|
319
|
+
* is unreachable in a browser, which is what keeps the innerHTML write out of
|
|
320
|
+
* the only place Trusted Types is enforced.
|
|
321
|
+
*
|
|
322
|
+
* Both parsers were checked to agree on the part that matters: the HTML
|
|
323
|
+
* parser's foreign-content adjustment restores the camel case of `viewBox`,
|
|
324
|
+
* `clipPath`, `linearGradient`, `gradientTransform`, `textPath` and puts
|
|
325
|
+
* `xlink:href` in the xlink namespace.
|
|
326
|
+
*/
|
|
327
|
+
function parseInert(doc, data) {
|
|
328
|
+
const parser = doc.defaultView?.DOMParser;
|
|
329
|
+
if (parser)
|
|
330
|
+
return firstSvg(new parser().parseFromString(data, 'text/html').body);
|
|
331
|
+
const template = doc.createElement('template');
|
|
332
|
+
template.innerHTML = data;
|
|
333
|
+
return firstSvg(template.content);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The first `<svg>` in document order, found by walking rather than by
|
|
337
|
+
* `querySelector`.
|
|
338
|
+
*
|
|
339
|
+
* Two reasons, and the second is the one that matters. A control that decides
|
|
340
|
+
* what may execute should not route that decision through a selector engine —
|
|
341
|
+
* `localName === 'svg'` is a comparison anyone can check, and CSS parsing is a
|
|
342
|
+
* layer with its own history. The first reason is merely that it is faster:
|
|
343
|
+
* jsdom's engine costs 4 ms on a document this size, which the unit suite pays
|
|
344
|
+
* on every rendered icon.
|
|
345
|
+
*/
|
|
346
|
+
function firstSvg(root) {
|
|
347
|
+
const children = root.childNodes;
|
|
348
|
+
for (let i = 0, n = children.length; i < n; i++) {
|
|
349
|
+
const child = children[i];
|
|
350
|
+
if (child.nodeType !== 1)
|
|
351
|
+
continue;
|
|
352
|
+
const element = child;
|
|
353
|
+
if (element.localName === 'svg')
|
|
354
|
+
return element;
|
|
355
|
+
const nested = firstSvg(element);
|
|
356
|
+
if (nested)
|
|
357
|
+
return nested;
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
/** Recreate one element, then its allowed children, in the live document. */
|
|
362
|
+
function copy(doc, source, report, depth) {
|
|
363
|
+
const el = doc.createElementNS(SVG_NS, source.localName);
|
|
364
|
+
const attributes = source.attributes;
|
|
365
|
+
for (let i = 0, n = attributes.length; i < n; i++) {
|
|
366
|
+
const { name, value } = attributes[i];
|
|
367
|
+
if (!isAllowedAttribute(name, value)) {
|
|
368
|
+
report?.attributes.add(name);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (name === 'xlink:href')
|
|
372
|
+
el.setAttributeNS(XLINK_NS, name, value);
|
|
373
|
+
else
|
|
374
|
+
el.setAttribute(name, value);
|
|
375
|
+
}
|
|
376
|
+
if (depth >= MAX_DEPTH) {
|
|
377
|
+
// Named, not silent — the cap is the one rule here that can bite an icon
|
|
378
|
+
// nobody meant as an attack, and a truncated glyph should say so.
|
|
379
|
+
if (source.childNodes.length > 0)
|
|
380
|
+
report?.elements.add(`… deeper than ${MAX_DEPTH} levels`);
|
|
381
|
+
return el;
|
|
382
|
+
}
|
|
383
|
+
const children = source.childNodes;
|
|
384
|
+
for (let i = 0, n = children.length; i < n; i++) {
|
|
385
|
+
const child = children[i];
|
|
386
|
+
// Text, so `<title>`, `<desc>` and `<text>` keep saying what they said.
|
|
387
|
+
// Comments, CDATA and processing instructions are dropped: an icon has no
|
|
388
|
+
// use for them and a comment is a place to hide a payload from a reader.
|
|
389
|
+
if (child.nodeType === 3) {
|
|
390
|
+
el.appendChild(doc.createTextNode(child.nodeValue ?? ''));
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (child.nodeType !== 1)
|
|
394
|
+
continue;
|
|
395
|
+
const element = child;
|
|
396
|
+
if (!ELEMENTS.has(element.localName)) {
|
|
397
|
+
report?.elements.add(element.localName);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
el.appendChild(copy(doc, element, report, depth + 1));
|
|
401
|
+
}
|
|
402
|
+
return el;
|
|
403
|
+
}
|
|
404
|
+
function isAllowedAttribute(name, value) {
|
|
405
|
+
// `data-*` is a styling and testing hook that can never be behavioural.
|
|
406
|
+
if (name.startsWith('data-'))
|
|
407
|
+
return true;
|
|
408
|
+
if (!ATTRIBUTES.has(name))
|
|
409
|
+
return false;
|
|
410
|
+
if (FRAGMENT_ONLY.has(name))
|
|
411
|
+
return FRAGMENT_RE.test(value.trim());
|
|
412
|
+
if (URL_FN_RE.test(value))
|
|
413
|
+
return URL_FRAGMENT_RE.test(value.trim());
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @license
|
|
419
|
+
*
|
|
420
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
421
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
422
|
+
*/
|
|
423
|
+
const VIEW_BOX_RE = /<svg\b[^>]*\bviewBox\s*=/i;
|
|
424
|
+
const SVG_ROOT_RE = /<svg\b/i;
|
|
425
|
+
/**
|
|
426
|
+
* Dev-mode sanity check for a registered icon — a **rendering** check, and
|
|
427
|
+
* explicitly not a security control.
|
|
428
|
+
*
|
|
429
|
+
* It warns about the two things that make an icon come out wrong: no `<svg>`
|
|
430
|
+
* root, and no `viewBox`, which stops the glyph scaling to the host. It reads
|
|
431
|
+
* the string with two regexes and cannot tell a shape from a script — every
|
|
432
|
+
* payload in the icon corpus passes it, because each one does carry an `<svg>`
|
|
433
|
+
* root with a `viewBox`. It also runs only under `isDevMode()`, so it is absent
|
|
434
|
+
* from the build an attacker would meet.
|
|
435
|
+
*
|
|
436
|
+
* What actually keeps hostile markup out of the DOM is {@link sanitizeIcon},
|
|
437
|
+
* which runs on every render in every build: `<wr-icon>` rebuilds the glyph
|
|
438
|
+
* from an allowlist instead of assigning to `innerHTML`. Anything this function
|
|
439
|
+
* would have to reject is already gone by then, and the component says so in
|
|
440
|
+
* dev mode when it drops something.
|
|
441
|
+
*
|
|
442
|
+
* Production builds drop the call entirely via `isDevMode()` tree-shaking.
|
|
443
|
+
*
|
|
444
|
+
* @internal
|
|
445
|
+
*/
|
|
446
|
+
function validateIcon(icon) {
|
|
447
|
+
if (!SVG_ROOT_RE.test(icon.data)) {
|
|
448
|
+
// eslint-disable-next-line no-console -- dev-mode validation
|
|
449
|
+
console.warn(`[NGWR] Icon "${icon.name}" data does not contain an <svg> root element. ` +
|
|
450
|
+
`Custom icons must be valid SVG markup.`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!VIEW_BOX_RE.test(icon.data)) {
|
|
454
|
+
// eslint-disable-next-line no-console -- dev-mode validation
|
|
455
|
+
console.warn(`[NGWR] Icon "${icon.name}" has no viewBox attribute — it won't scale ` +
|
|
456
|
+
`to the host size. Add viewBox="0 0 W H" to the <svg> root.`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
63
460
|
/**
|
|
64
461
|
* @license
|
|
65
462
|
*
|
|
@@ -83,6 +480,7 @@ class WrIcon {
|
|
|
83
480
|
name = input.required(/* @ts-ignore */
|
|
84
481
|
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
85
482
|
host = inject(ElementRef);
|
|
483
|
+
doc = inject(DOCUMENT);
|
|
86
484
|
/**
|
|
87
485
|
* Every icon visible from this position, walked up the whole chain of
|
|
88
486
|
* registering injector levels.
|
|
@@ -118,17 +516,74 @@ class WrIcon {
|
|
|
118
516
|
`If the name IS registered, check the injector level: a component-level provideWrIcons() ` +
|
|
119
517
|
`shadows icons registered on an ancestor component.`);
|
|
120
518
|
}
|
|
121
|
-
this.
|
|
519
|
+
this.clear();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Rebuilt from an allowlist rather than assigned to `innerHTML`, which
|
|
523
|
+
// ran an `onload` on the root, an `<image onerror>`, an `<img>` smuggled
|
|
524
|
+
// in after a closed `</svg>` and anything inside a `<foreignObject>` —
|
|
525
|
+
// in production builds, with the app's own origin. `provideWrIcons()`
|
|
526
|
+
// data is untrusted the same way `<wr-markdown>`'s input is: a tenant
|
|
527
|
+
// logo or a fetched icon pack is a normal way to fill it. Building nodes
|
|
528
|
+
// also keeps `<wr-icon>` clear of Trusted Types, which refuses the
|
|
529
|
+
// `innerHTML` write outright.
|
|
530
|
+
const report = isDevMode() ? { elements: new Set(), attributes: new Set() } : undefined;
|
|
531
|
+
const svg = sanitizeIcon(this.doc, icon.data, report);
|
|
532
|
+
if (!svg) {
|
|
533
|
+
if (isDevMode()) {
|
|
534
|
+
// eslint-disable-next-line no-console -- misconfiguration must be visible, but must not throw
|
|
535
|
+
console.error(`[NGWR] Icon "${name}" has no <svg> root element and cannot be rendered. ` +
|
|
536
|
+
`Custom icons must be valid SVG markup.`);
|
|
537
|
+
}
|
|
538
|
+
this.clear();
|
|
122
539
|
return;
|
|
123
540
|
}
|
|
124
|
-
this.host.nativeElement.innerHTML = icon.data;
|
|
125
541
|
// Every hand-written inline SVG in the library carries aria-hidden;
|
|
126
542
|
// registered icons went in without it, so a decorative glyph could be
|
|
127
543
|
// announced — usually as a bare "graphic" beside the label it decorates.
|
|
128
544
|
// Consumers that need a name put it on the interactive host instead.
|
|
129
|
-
|
|
545
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
546
|
+
this.clear();
|
|
547
|
+
this.host.nativeElement.appendChild(svg);
|
|
548
|
+
if (report)
|
|
549
|
+
this.reportStripped(name, icon, report);
|
|
130
550
|
});
|
|
131
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* `replaceChildren()` would say this in one call, but domino — the DOM
|
|
554
|
+
* `@angular/platform-server` renders into — does not implement it, and
|
|
555
|
+
* `innerHTML = ''` is the write Trusted Types refuses.
|
|
556
|
+
*/
|
|
557
|
+
clear() {
|
|
558
|
+
const el = this.host.nativeElement;
|
|
559
|
+
while (el.firstChild)
|
|
560
|
+
el.removeChild(el.firstChild);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Say what the allowlist removed, once per registered icon.
|
|
564
|
+
*
|
|
565
|
+
* A silently stripped icon is the failure mode this rebuild could introduce:
|
|
566
|
+
* a consumer's own glyph renders blank or wrong and nothing anywhere says
|
|
567
|
+
* why. Keyed on the definition object, so a page with fifty instances of the
|
|
568
|
+
* same icon warns once and an icon nobody renders never warns at all.
|
|
569
|
+
*/
|
|
570
|
+
reportStripped(name, icon, report) {
|
|
571
|
+
if (WrIcon.reported.has(icon))
|
|
572
|
+
return;
|
|
573
|
+
if (report.elements.size === 0 && report.attributes.size === 0)
|
|
574
|
+
return;
|
|
575
|
+
WrIcon.reported.add(icon);
|
|
576
|
+
const removed = [
|
|
577
|
+
report.elements.size ? `elements: ${[...report.elements].join(', ')}` : '',
|
|
578
|
+
report.attributes.size ? `attributes: ${[...report.attributes].join(', ')}` : '',
|
|
579
|
+
].filter(Boolean);
|
|
580
|
+
// eslint-disable-next-line no-console -- a stripped icon must not be silent
|
|
581
|
+
console.warn(`[NGWR] Icon "${name}" was rendered without part of its markup — ${removed.join('; ')}. ` +
|
|
582
|
+
`Registered icon data is treated as untrusted and rebuilt from an allowlist of SVG ` +
|
|
583
|
+
`shape, paint and text elements, so event handlers, <script>, <style>, <image>, ` +
|
|
584
|
+
`<foreignObject>, <a>, SMIL animation and non-fragment hrefs never reach the DOM.`);
|
|
585
|
+
}
|
|
586
|
+
static reported = new WeakSet();
|
|
132
587
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
133
588
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: WrIcon, isStandalone: true, selector: "wr-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-icon": "name()" }, classAttribute: "wr-icon" }, ngImport: i0, template: '', isInline: true, encapsulation: i0.ViewEncapsulation.None });
|
|
134
589
|
}
|
|
@@ -145,39 +600,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
|
|
|
145
600
|
}]
|
|
146
601
|
}], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }] } });
|
|
147
602
|
|
|
148
|
-
/**
|
|
149
|
-
* @license
|
|
150
|
-
*
|
|
151
|
-
* Use of this source code is governed by an MIT-style license that can be
|
|
152
|
-
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
153
|
-
*/
|
|
154
|
-
const VIEW_BOX_RE = /<svg\b[^>]*\bviewBox\s*=/i;
|
|
155
|
-
const SVG_ROOT_RE = /<svg\b/i;
|
|
156
|
-
/**
|
|
157
|
-
* Dev-mode sanity check for a registered icon.
|
|
158
|
-
*
|
|
159
|
-
* Warns about issues that prevent correct rendering — most importantly,
|
|
160
|
-
* a missing `viewBox`, which causes the SVG not to scale to the icon
|
|
161
|
-
* host's size.
|
|
162
|
-
*
|
|
163
|
-
* Production builds drop the call entirely via `isDevMode()` tree-shaking.
|
|
164
|
-
*
|
|
165
|
-
* @internal
|
|
166
|
-
*/
|
|
167
|
-
function validateIcon(icon) {
|
|
168
|
-
if (!SVG_ROOT_RE.test(icon.data)) {
|
|
169
|
-
// eslint-disable-next-line no-console -- dev-mode validation
|
|
170
|
-
console.warn(`[NGWR] Icon "${icon.name}" data does not contain an <svg> root element. ` +
|
|
171
|
-
`Custom icons must be valid SVG markup.`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (!VIEW_BOX_RE.test(icon.data)) {
|
|
175
|
-
// eslint-disable-next-line no-console -- dev-mode validation
|
|
176
|
-
console.warn(`[NGWR] Icon "${icon.name}" has no viewBox attribute — it won't scale ` +
|
|
177
|
-
`to the host size. Add viewBox="0 0 W H" to the <svg> root.`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
603
|
/**
|
|
182
604
|
* @license
|
|
183
605
|
*
|
|
@@ -194,7 +616,13 @@ function validateIcon(icon) {
|
|
|
194
616
|
*
|
|
195
617
|
* In dev mode, each icon is validated for common issues (missing
|
|
196
618
|
* `viewBox`, malformed root). Validation is dropped from production
|
|
197
|
-
* builds via `isDevMode()` tree-shaking
|
|
619
|
+
* builds via `isDevMode()` tree-shaking, and it is a rendering check rather
|
|
620
|
+
* than a security one — see {@link validateIcon}.
|
|
621
|
+
*
|
|
622
|
+
* Icon `data` is treated as **untrusted** at render: `<wr-icon>` rebuilds the
|
|
623
|
+
* glyph from an allowlist instead of writing the string to `innerHTML`, so a
|
|
624
|
+
* set fetched at runtime or supplied by a tenant cannot execute in the app's
|
|
625
|
+
* origin. Dev mode names anything the allowlist removed.
|
|
198
626
|
*
|
|
199
627
|
* @example
|
|
200
628
|
* ```ts
|
|
@@ -272,6 +700,15 @@ function provideWrIcons(icons) {
|
|
|
272
700
|
* ],
|
|
273
701
|
* });
|
|
274
702
|
* ```
|
|
703
|
+
*
|
|
704
|
+
* **`svg` is treated as untrusted.** It is a string, so it is a natural place
|
|
705
|
+
* for a tenant's uploaded logo or an icon pack fetched from an API to end up,
|
|
706
|
+
* and `<wr-icon>` used to write it to `innerHTML`. It no longer does: the glyph
|
|
707
|
+
* is rebuilt from an allowlist of SVG shape, paint and text elements, so
|
|
708
|
+
* `<script>`, `<style>`, `<image>`, `<foreignObject>`, `<a>`, SMIL animation,
|
|
709
|
+
* every `on*` handler, `style` attributes and non-fragment `href`s are dropped
|
|
710
|
+
* before anything reaches the DOM. In dev mode the component names what it
|
|
711
|
+
* removed, so a legitimate icon that loses something says so out loud.
|
|
275
712
|
*/
|
|
276
713
|
function svgIcon(name, svg) {
|
|
277
714
|
return { name, data: svg };
|