@navette-js/support 1.0.0-beta.3 → 1.0.0-beta.4
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/THIRD-PARTY-NOTICES.md +1 -1
- package/icons/filament/LICENSE.md +21 -0
- package/icons/filament/o-arrows-move.svg +11 -0
- package/icons/filament/o-crop.svg +5 -0
- package/icons/filament/o-flip-horizontal.svg +3 -0
- package/icons/filament/o-flip-vertical.svg +3 -0
- package/lib/html.d.ts +12 -0
- package/lib/html.js +180 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +3 -1
- package/package.json +1 -1
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -4,7 +4,7 @@ Navette is an independent project, not affiliated with or endorsed by the Filame
|
|
|
4
4
|
|
|
5
5
|
| Work | Used for | License | License file |
|
|
6
6
|
|---|---|---|---|
|
|
7
|
-
| [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup derived from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
|
|
7
|
+
| [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup derived from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`), image editor icons (`@navette-js/support/icons/filament`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md`, `packages/support/icons/filament/LICENSE.md` |
|
|
8
8
|
| [Livewire](https://github.com/livewire/livewire) © Caleb Porzio | Client script `livewire.js` (`@navette-js/wire/vendor`); the server runtime follows its protocol | MIT | `packages/wire/vendor/LIVEWIRE-LICENSE.md` |
|
|
9
9
|
| [Heroicons](https://github.com/tailwindlabs/heroicons) | SVG icons (`@navette-js/support/icons`) | MIT | `packages/support/icons/HEROICONS-LICENSE.md` |
|
|
10
10
|
| [Inter](https://github.com/rsms/inter) © The Inter Project Authors | Default font files (`@navette-js/panels/dist/filament/fonts`) | SIL Open Font License 1.1 | `packages/panels/dist/filament/fonts/INTER-LICENSE.txt` |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Filament
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-slot="icon">
|
|
2
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
3
|
+
<path d="M18 9l3 3l-3 3" />
|
|
4
|
+
<path d="M15 12h6" />
|
|
5
|
+
<path d="M6 9l-3 3l3 3" />
|
|
6
|
+
<path d="M3 12h6" />
|
|
7
|
+
<path d="M9 18l3 3l3 -3" />
|
|
8
|
+
<path d="M12 15v6" />
|
|
9
|
+
<path d="M15 6l-3 -3l-3 3" />
|
|
10
|
+
<path d="M12 3v6" />
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-slot="icon">
|
|
2
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
3
|
+
<path d="M8 5v10a1 1 0 0 0 1 1h10" />
|
|
4
|
+
<path d="M5 8h10a1 1 0 0 1 1 1v10" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" data-slot="icon">
|
|
2
|
+
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m17 3l-5 5l-5-5h10m0 18l-5-5l-5 5h10M4 12H2m8 0H8m8 0h-2m8 0h-2"/>
|
|
3
|
+
</svg>
|
package/lib/html.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Link targets: http(s), mailto, site paths, anchors and relative paths; never `javascript:`, `data:` or another scheme. */
|
|
2
|
+
export declare function isSafeUrl(value: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Removes everything but a small set of formatting elements (paragraphs, emphasis, lists, links, headings, code...) and
|
|
5
|
+
* their safe attributes. No `style`, `class`, event handler, image or form survives, and links only keep safe URLs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeHtml(html: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Renders Markdown (paragraphs, headings, lists, quotes, code blocks, emphasis, links). Raw HTML is never passed through:
|
|
10
|
+
* it is shown as text. Links only keep safe URLs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function markdownToHtml(markdown: string): string;
|
package/lib/html.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// HTML from application data: an allowlist sanitizer, and a small Markdown renderer whose output is safe by construction.
|
|
2
|
+
/** Allowed elements and, for each one, its allowed attributes. Everything else is removed (the text inside is kept). */
|
|
3
|
+
const ALLOWED = {
|
|
4
|
+
a: ['href', 'title'], abbr: ['title'], b: [], blockquote: [], br: [], code: [], del: [], div: [], em: [], h1: [], h2: [], h3: [], h4: [], h5: [], h6: [],
|
|
5
|
+
hr: [], i: [], kbd: [], li: [], mark: [], ol: ['start'], p: [], pre: [], s: [], small: [], span: [], strong: [], sub: [], sup: [], u: [], ul: [],
|
|
6
|
+
};
|
|
7
|
+
const VOID = new Set(['br', 'hr']);
|
|
8
|
+
/** Elements removed together with their content. */
|
|
9
|
+
const DROPPED = new Set(['script', 'style', 'iframe', 'object', 'embed', 'template', 'textarea', 'title', 'noscript', 'svg', 'math', 'select', 'head', 'frame', 'frameset', 'applet', 'noembed', 'xmp', 'plaintext']);
|
|
10
|
+
const escapeAttribute = (value) => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
11
|
+
/** Text between tags: entities are kept, anything else that could start markup is escaped. */
|
|
12
|
+
const escapeText = (text) => text.replace(/&(?!(?:#\d{1,7}|#x[0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});)/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
+
const decodeEntities = (value) => value
|
|
14
|
+
.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(Math.min(Number.parseInt(hex, 16), 0x10ffff)))
|
|
15
|
+
.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Math.min(Number(dec), 0x10ffff)))
|
|
16
|
+
.replace(/:/gi, ':').replace(/&tab;/gi, '\t').replace(/&newline;/gi, '\n').replace(/&/gi, '&');
|
|
17
|
+
/** Link targets: http(s), mailto, site paths, anchors and relative paths; never `javascript:`, `data:` or another scheme. */
|
|
18
|
+
export function isSafeUrl(value) {
|
|
19
|
+
const url = decodeEntities(value).replace(/[\u0000- \u007f-\u009f]/g, '');
|
|
20
|
+
if (/^(https?:|mailto:)/i.test(url))
|
|
21
|
+
return true;
|
|
22
|
+
if (url.startsWith('//'))
|
|
23
|
+
return false;
|
|
24
|
+
// No scheme at all: a colon may only appear after a `/`, `?` or `#`.
|
|
25
|
+
return !/^[^/?#]*:/.test(url);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Removes everything but a small set of formatting elements (paragraphs, emphasis, lists, links, headings, code...) and
|
|
29
|
+
* their safe attributes. No `style`, `class`, event handler, image or form survives, and links only keep safe URLs.
|
|
30
|
+
*/
|
|
31
|
+
export function sanitizeHtml(html) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const open = [];
|
|
34
|
+
const source = String(html ?? '');
|
|
35
|
+
let position = 0;
|
|
36
|
+
while (position < source.length) {
|
|
37
|
+
const next = source.indexOf('<', position);
|
|
38
|
+
if (next < 0) {
|
|
39
|
+
out.push(escapeText(source.slice(position)));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
out.push(escapeText(source.slice(position, next)));
|
|
43
|
+
position = next;
|
|
44
|
+
const rest = source.slice(position);
|
|
45
|
+
if (rest.startsWith('<!--')) {
|
|
46
|
+
const end = source.indexOf('-->', position + 4);
|
|
47
|
+
position = end < 0 ? source.length : end + 3;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const tag = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:[^<>"']|"[^"]*"|'[^']*')*)>/.exec(rest);
|
|
51
|
+
if (!tag) {
|
|
52
|
+
out.push('<');
|
|
53
|
+
position++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
position += tag[0].length;
|
|
57
|
+
const [, closing, rawName, rawAttributes] = tag;
|
|
58
|
+
const name = rawName.toLowerCase();
|
|
59
|
+
if (DROPPED.has(name)) {
|
|
60
|
+
if (!closing) {
|
|
61
|
+
const end = source.toLowerCase().indexOf(`</${name}`, position);
|
|
62
|
+
position = end < 0 ? source.length : (source.indexOf('>', end) < 0 ? source.length : source.indexOf('>', end) + 1);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!Object.hasOwn(ALLOWED, name))
|
|
67
|
+
continue;
|
|
68
|
+
if (closing) {
|
|
69
|
+
const index = open.lastIndexOf(name);
|
|
70
|
+
if (index >= 0)
|
|
71
|
+
while (open.length > index)
|
|
72
|
+
out.push(`</${open.pop()}>`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const attributes = [];
|
|
76
|
+
for (const match of rawAttributes.matchAll(/([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+)))?/g)) {
|
|
77
|
+
const attribute = match[1].toLowerCase();
|
|
78
|
+
const value = match[2] ?? match[3] ?? match[4] ?? '';
|
|
79
|
+
if (!ALLOWED[name].includes(attribute) || attributes.some((existing) => existing.startsWith(`${attribute}=`)))
|
|
80
|
+
continue;
|
|
81
|
+
if (attribute === 'href' && !isSafeUrl(value))
|
|
82
|
+
continue;
|
|
83
|
+
if (attribute === 'start' && !/^\d{1,9}$/.test(value))
|
|
84
|
+
continue;
|
|
85
|
+
attributes.push(`${attribute}="${escapeAttribute(decodeEntities(value))}"`);
|
|
86
|
+
}
|
|
87
|
+
if (name === 'a' && attributes.some((attribute) => attribute.startsWith('href=')))
|
|
88
|
+
attributes.push('rel="noopener noreferrer nofollow"');
|
|
89
|
+
out.push(`<${name}${attributes.length ? ` ${attributes.join(' ')}` : ''}>`);
|
|
90
|
+
if (!VOID.has(name))
|
|
91
|
+
open.push(name);
|
|
92
|
+
}
|
|
93
|
+
while (open.length)
|
|
94
|
+
out.push(`</${open.pop()}>`);
|
|
95
|
+
return out.join('');
|
|
96
|
+
}
|
|
97
|
+
// ------------------------------------------------------------------ Markdown
|
|
98
|
+
const escapeHtml = (value) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
99
|
+
/** Inline Markdown: code spans, links, bold, italic, strikethrough and hard line breaks. The text is escaped first. */
|
|
100
|
+
function inlineMarkdown(source) {
|
|
101
|
+
const codes = [];
|
|
102
|
+
let text = source.replace(/`([^`]+)`/g, (_, code) => `\u0000${codes.push(`<code>${escapeHtml(code)}</code>`) - 1}\u0000`);
|
|
103
|
+
text = escapeHtml(text);
|
|
104
|
+
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^&]*)")?\)/g, (_, label, href, title) => {
|
|
105
|
+
const url = href.replace(/&/g, '&');
|
|
106
|
+
if (!isSafeUrl(url))
|
|
107
|
+
return label;
|
|
108
|
+
return `<a href="${escapeHtml(url)}"${title ? ` title="${title}"` : ''} rel="noopener noreferrer nofollow">${label}</a>`;
|
|
109
|
+
});
|
|
110
|
+
// Bare URLs become links; they stop before an escaped quote or angle bracket.
|
|
111
|
+
text = text.replace(/(^|[\s(])(https?:\/\/(?:(?!"|<|>)[^\s<])*(?:(?!"|<|>)[^\s<.,;:!?)]))/g, (_, before, url) => `${before}<a href="${url}" rel="noopener noreferrer nofollow">${url}</a>`);
|
|
112
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/__([^_]+)__/g, '<strong>$1</strong>')
|
|
113
|
+
.replace(/(^|[^\w*])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>').replace(/(^|[^\w])_([^_\s][^_]*)_(?=[^\w]|$)/g, '$1<em>$2</em>')
|
|
114
|
+
.replace(/~~([^~]+)~~/g, '<del>$1</del>').replace(/(?: {2,}|\\)\n/g, '<br>\n');
|
|
115
|
+
return text.replace(/\u0000(\d+)\u0000/g, (_, index) => codes[Number(index)]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Renders Markdown (paragraphs, headings, lists, quotes, code blocks, emphasis, links). Raw HTML is never passed through:
|
|
119
|
+
* it is shown as text. Links only keep safe URLs.
|
|
120
|
+
*/
|
|
121
|
+
export function markdownToHtml(markdown) {
|
|
122
|
+
const lines = String(markdown ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
123
|
+
const out = [];
|
|
124
|
+
let index = 0;
|
|
125
|
+
const isBlockStart = (line) => /^(#{1,6}\s|>\s?|```|\s*([-*+]|\d{1,9}[.)])\s+|\s*([-*_])(\s*\3){2,}\s*$)/.test(line);
|
|
126
|
+
while (index < lines.length) {
|
|
127
|
+
const line = lines[index];
|
|
128
|
+
if (line.trim() === '') {
|
|
129
|
+
index++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (line.startsWith('```')) {
|
|
133
|
+
const code = [];
|
|
134
|
+
index++;
|
|
135
|
+
while (index < lines.length && !lines[index].startsWith('```'))
|
|
136
|
+
code.push(lines[index++]);
|
|
137
|
+
index++;
|
|
138
|
+
out.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const heading = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
|
|
142
|
+
if (heading) {
|
|
143
|
+
out.push(`<h${heading[1].length}>${inlineMarkdown(heading[2])}</h${heading[1].length}>`);
|
|
144
|
+
index++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
148
|
+
out.push('<hr>');
|
|
149
|
+
index++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/^>\s?/.test(line)) {
|
|
153
|
+
const quote = [];
|
|
154
|
+
while (index < lines.length && /^>\s?/.test(lines[index]))
|
|
155
|
+
quote.push(lines[index++].replace(/^>\s?/, ''));
|
|
156
|
+
out.push(`<blockquote>${markdownToHtml(quote.join('\n'))}</blockquote>`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const listItem = /^\s*([-*+]|(\d{1,9})[.)])\s+(.*)$/.exec(line);
|
|
160
|
+
if (listItem) {
|
|
161
|
+
const ordered = listItem[2] !== undefined;
|
|
162
|
+
const items = [];
|
|
163
|
+
while (index < lines.length) {
|
|
164
|
+
const match = /^\s*([-*+]|(\d{1,9})[.)])\s+(.*)$/.exec(lines[index]);
|
|
165
|
+
if (!match || (match[2] !== undefined) !== ordered)
|
|
166
|
+
break;
|
|
167
|
+
items.push(`<li>${inlineMarkdown(match[3])}</li>`);
|
|
168
|
+
index++;
|
|
169
|
+
}
|
|
170
|
+
const start = ordered && listItem[2] !== '1' ? ` start="${Number(listItem[2])}"` : '';
|
|
171
|
+
out.push(ordered ? `<ol${start}>${items.join('')}</ol>` : `<ul>${items.join('')}</ul>`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const paragraph = [];
|
|
175
|
+
while (index < lines.length && lines[index].trim() !== '' && (paragraph.length === 0 || !isBlockStart(lines[index])))
|
|
176
|
+
paragraph.push(lines[index++]);
|
|
177
|
+
out.push(`<p>${inlineMarkdown(paragraph.join('\n'))}</p>`);
|
|
178
|
+
}
|
|
179
|
+
return out.join('');
|
|
180
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -162,4 +162,4 @@ export declare function runWithRenderHooks<T>(registry: RenderHookRegistry, fn:
|
|
|
162
162
|
/** Renders a slot: process-wide hooks, then the current panel's hooks. */
|
|
163
163
|
export declare function renderHook(name: string, scopes?: unknown | unknown[] | null, data?: Record<string, any>): string;
|
|
164
164
|
export declare function hasRenderHook(name: string, scopes?: unknown | unknown[] | null): boolean;
|
|
165
|
-
export {};
|
|
165
|
+
export { isSafeUrl, markdownToHtml, sanitizeHtml } from './html.ts';
|
package/lib/index.js
CHANGED
|
@@ -391,7 +391,8 @@ const iconCache = new Map();
|
|
|
391
391
|
export function icon(name, options = {}) {
|
|
392
392
|
let svg = iconCache.get(name);
|
|
393
393
|
if (svg === undefined) {
|
|
394
|
-
|
|
394
|
+
// `fi-o-crop`: Filament's own icons (MIT), shipped next to Heroicons.
|
|
395
|
+
const file = name.startsWith('fi-') ? here(`../icons/filament/${name.slice(3).replace(/[^a-z0-9-]/g, '')}.svg`) : here(`../icons/heroicons/${name.replace(/^heroicon-/, '').replace(/[^a-z0-9-]/g, '')}.svg`);
|
|
395
396
|
if (!existsSync(file))
|
|
396
397
|
throw new Error(`Icon not found: ${name}`);
|
|
397
398
|
svg = readFileSync(file, 'utf8').trim();
|
|
@@ -512,3 +513,4 @@ export function hasRenderHook(name, scopes = null) {
|
|
|
512
513
|
return activeRegistries().some((registry) => registry.has(name, scopes));
|
|
513
514
|
}
|
|
514
515
|
views.share({ renderHook, hasRenderHook });
|
|
516
|
+
export { isSafeUrl, markdownToHtml, sanitizeHtml } from "./html.js";
|