tosijs-ui 1.16.23 → 1.16.24
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/CHANGELOG.md +32 -0
- package/bin/format.ts +12 -6
- package/dist/doc-system/site/dev-auth.d.ts +38 -0
- package/dist/doc-system/site/dev-auth.js +51 -0
- package/dist/doc-system/site/dev-server.js +52 -24
- package/dist/doc-system/site/site-config.d.ts +3 -2
- package/dist/icon-rules.d.ts +4 -2
- package/dist/icon-rules.js +18 -5
- package/dist/icons.js +10 -5
- package/dist/iife.js +91 -91
- package/dist/iife.js.map +8 -7
- package/dist/live-example/component.js +13 -3
- package/dist/live-example/source-access.d.ts +12 -0
- package/dist/live-example/source-access.js +46 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.24
|
|
4
|
+
|
|
5
|
+
`dist/iife.js` grows by about 0.2 kB gzip.
|
|
6
|
+
|
|
7
|
+
- **Composition rules chain.** A rule may apply to another rule, so `un:check:square` slashes
|
|
8
|
+
the checked square, and `unCheckLock` draws instead of the placeholder. `iconSvg` reports a
|
|
9
|
+
chained rule as needing the renderer, as it does any rule. Chains stop at four rules deep,
|
|
10
|
+
which keeps a hostile icon name cheap to resolve, and a custom rule whose prefix can match
|
|
11
|
+
nothing no longer recurses. `defineIcons` may now warn about a new name that reads as a
|
|
12
|
+
chain (`unCheckFoo`); it is a warning, and your definition still wins.
|
|
13
|
+
- **The icon composition demo shows the bracket form.** It printed `icons.check:square()`,
|
|
14
|
+
which is not JavaScript; a name with a `:` now shows as `icons['check:square']()`.
|
|
15
|
+
- **Prettier never formats SVG files.** `*.svg` is in `.prettierignore` and a `requirePragma`
|
|
16
|
+
override, and `tosijs-format` excludes it on the command line as it does markdown. Prettier
|
|
17
|
+
has no SVG parser of its own, but an editor association or a plugin such as
|
|
18
|
+
`prettier-plugin-xml` would reflow a drawing tool's export. Adopters using `tosijs-format`
|
|
19
|
+
get the command-line exclusion. To cover editors too, add `*.svg` to `.prettierignore` and
|
|
20
|
+
`{ "files": ["*.svg"], "options": { "requirePragma": true } }` to your Prettier `overrides`.
|
|
21
|
+
|
|
22
|
+
### Dev-server access
|
|
23
|
+
|
|
24
|
+
- **A live example's "Save to source" is offered wherever the dev server admits the page**:
|
|
25
|
+
a LAN device under `trustLan`, or a tunnel visitor with an edit session, not only
|
|
26
|
+
`localhost`. The dev server marks the pages it serves with `editableSources` on, and an
|
|
27
|
+
example on such a page asks with a `HEAD` request when it connects, once per source file; it
|
|
28
|
+
believes only an answer carrying the dev server's own header. A deployed static site is never
|
|
29
|
+
asked.
|
|
30
|
+
- **Every source write admitted by `trustLan` is logged** with the device's address, the file
|
|
31
|
+
(quoted, so a crafted name cannot forge a log line) and whether it was saved.
|
|
32
|
+
- **The dev server suggests `trustLan` only to a refused device on a private network**, the
|
|
33
|
+
one case it would admit; anyone else is pointed at an edit link.
|
|
34
|
+
|
|
3
35
|
## 1.16.23
|
|
4
36
|
|
|
5
37
|
- **`rule:icon` applies an icon composition rule explicitly.** A `:` marks the boundary
|
package/bin/format.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/*
|
|
3
3
|
tosijs-format — the house format step, shipped as a command (#187).
|
|
4
4
|
|
|
5
|
-
tosijs-format eslint --fix, then prettier --write (markdown excluded)
|
|
6
|
-
tosijs-format --check eslint, then prettier --check (markdown excluded); exit 1 on any finding
|
|
5
|
+
tosijs-format eslint --fix, then prettier --write (markdown and SVG excluded)
|
|
6
|
+
tosijs-format --check eslint, then prettier --check (markdown and SVG excluded); exit 1 on any finding
|
|
7
7
|
|
|
8
8
|
**Prettier never touches markdown** in this stack. The rule itself lives in the Prettier
|
|
9
9
|
CONFIG, where every entry point reads it (editors, lint-staged, a bare `prettier --write`):
|
|
@@ -31,10 +31,11 @@ import { parseArgv } from './resolve-site-config.ts'
|
|
|
31
31
|
|
|
32
32
|
const { has } = parseArgv(process.argv.slice(2), {
|
|
33
33
|
bin: 'tosijs-format',
|
|
34
|
-
summary:
|
|
34
|
+
summary:
|
|
35
|
+
'the house format step: eslint, then prettier (never markdown or SVG)',
|
|
35
36
|
flags: ['check'],
|
|
36
37
|
usage:
|
|
37
|
-
` tosijs-format eslint --fix ., then prettier --write . (markdown excluded)\n` +
|
|
38
|
+
` tosijs-format eslint --fix ., then prettier --write . (markdown and SVG excluded)\n` +
|
|
38
39
|
` tosijs-format --check check only: exit 1 on anything unformatted or any lint error\n\n` +
|
|
39
40
|
`Uses the project's own prettier and eslint (node_modules/.bin). ESLint runs only when the\n` +
|
|
40
41
|
`project has an eslint.config.* file.`,
|
|
@@ -81,13 +82,18 @@ if (hasEslintConfig) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
/*
|
|
86
|
+
The exclusion is the point of this command: prettier never touches markdown, or SVG.
|
|
87
|
+
Prettier has no SVG parser of its own, but an editor association or a plugin
|
|
88
|
+
(prettier-plugin-xml) gives it one, and an SVG is a drawing tool's export, not source to
|
|
89
|
+
reflow.
|
|
90
|
+
*/
|
|
85
91
|
if (
|
|
86
92
|
run([
|
|
87
93
|
bin('prettier'),
|
|
88
94
|
check ? '--check' : '--write',
|
|
89
95
|
'.',
|
|
90
|
-
'!**/*.{md,markdown,mdx}',
|
|
96
|
+
'!**/*.{md,markdown,mdx,svg}',
|
|
91
97
|
]) !== 0
|
|
92
98
|
) {
|
|
93
99
|
failed = true
|
|
@@ -295,6 +295,44 @@ export declare function mayWriteSource(opts: {
|
|
|
295
295
|
/** did the request carry forwarding headers? A proxied request is never LAN trust */
|
|
296
296
|
proxied?: boolean;
|
|
297
297
|
}): boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Who a `/__docstore/source` request is admitted as, or null to refuse it.
|
|
300
|
+
*
|
|
301
|
+
* The WHOLE gate for that endpoint, taking the real `Request`: `mayWriteSource` on the peer and
|
|
302
|
+
* listener, `trustLan` refused through a proxy, and the same-origin check (#90) on the direct
|
|
303
|
+
* path. It was inline in the server closure, where the 1.16.22 review could only verify LAN
|
|
304
|
+
* trust by hand (#3346); here it is tested with the headers a browser actually sends.
|
|
305
|
+
*
|
|
306
|
+
* `'lan'` is a write admitted only because the project trusts its LAN, which the server logs.
|
|
307
|
+
*/
|
|
308
|
+
export declare function sourceRequestAdmission(request: {
|
|
309
|
+
url?: string;
|
|
310
|
+
headers: {
|
|
311
|
+
get(name: string): string | null;
|
|
312
|
+
};
|
|
313
|
+
}, opts: {
|
|
314
|
+
viaTunnel: boolean;
|
|
315
|
+
peer?: string | null;
|
|
316
|
+
auth: AuthState;
|
|
317
|
+
trustLan?: boolean;
|
|
318
|
+
now: number;
|
|
319
|
+
}): 'loopback' | 'lan' | 'session' | null;
|
|
320
|
+
/**
|
|
321
|
+
* Whether to tell a refused source request about `trustLan`: only a direct, unproxied device on
|
|
322
|
+
* a private network, the one case turning it on would admit, and only while it is off. Anyone
|
|
323
|
+
* else is pointed at an edit link, which is what would help them.
|
|
324
|
+
*/
|
|
325
|
+
export declare function suggestsTrustLan(opts: {
|
|
326
|
+
viaTunnel: boolean;
|
|
327
|
+
peer?: string | null;
|
|
328
|
+
proxied: boolean;
|
|
329
|
+
trustLan: boolean;
|
|
330
|
+
}): boolean;
|
|
331
|
+
/**
|
|
332
|
+
* The log line for a source write admitted only by `trustLan`. The file name is the client's,
|
|
333
|
+
* so it is quoted as JSON: a newline or an escape sequence in it cannot forge a log line.
|
|
334
|
+
*/
|
|
335
|
+
export declare function trustLanWriteRecord(peer: string | null | undefined, file: unknown, status: number): string;
|
|
298
336
|
/**
|
|
299
337
|
* May the holder of this session token write?
|
|
300
338
|
*
|
|
@@ -658,6 +658,57 @@ export function mayWriteSource(opts) {
|
|
|
658
658
|
opts.proxied !== true &&
|
|
659
659
|
isPrivateNetworkAddress(opts.peer));
|
|
660
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Who a `/__docstore/source` request is admitted as, or null to refuse it.
|
|
663
|
+
*
|
|
664
|
+
* The WHOLE gate for that endpoint, taking the real `Request`: `mayWriteSource` on the peer and
|
|
665
|
+
* listener, `trustLan` refused through a proxy, and the same-origin check (#90) on the direct
|
|
666
|
+
* path. It was inline in the server closure, where the 1.16.22 review could only verify LAN
|
|
667
|
+
* trust by hand (#3346); here it is tested with the headers a browser actually sends.
|
|
668
|
+
*
|
|
669
|
+
* `'lan'` is a write admitted only because the project trusts its LAN, which the server logs.
|
|
670
|
+
*/
|
|
671
|
+
export function sourceRequestAdmission(request, opts) {
|
|
672
|
+
const session = readCookie(request.headers.get('cookie'), SESSION_COOKIE);
|
|
673
|
+
const allowed = mayWriteSource({
|
|
674
|
+
viaTunnel: opts.viaTunnel,
|
|
675
|
+
peer: opts.peer,
|
|
676
|
+
hasValidSession: validSessionCookie(opts.auth, session, opts.now),
|
|
677
|
+
// A share link's session may browse and may not save (asked AFTER validity, which is
|
|
678
|
+
// the call-order `cookieMayWrite` documents).
|
|
679
|
+
sessionMayWrite: cookieMayWrite(opts.auth, session),
|
|
680
|
+
trustLan: opts.trustLan === true,
|
|
681
|
+
proxied: isProxiedRequest(request.headers),
|
|
682
|
+
}) &&
|
|
683
|
+
// CSRF (#90): the direct path is peer-address-only. The tunnel path is legitimately
|
|
684
|
+
// cross-site and is gated by the session cookie instead.
|
|
685
|
+
(opts.viaTunnel || isSameOriginRequest(request));
|
|
686
|
+
if (!allowed)
|
|
687
|
+
return null;
|
|
688
|
+
if (opts.viaTunnel)
|
|
689
|
+
return 'session';
|
|
690
|
+
return isLoopbackAddressForAuth(opts.peer) ? 'loopback' : 'lan';
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Whether to tell a refused source request about `trustLan`: only a direct, unproxied device on
|
|
694
|
+
* a private network, the one case turning it on would admit, and only while it is off. Anyone
|
|
695
|
+
* else is pointed at an edit link, which is what would help them.
|
|
696
|
+
*/
|
|
697
|
+
export function suggestsTrustLan(opts) {
|
|
698
|
+
return (!opts.trustLan &&
|
|
699
|
+
!opts.viaTunnel &&
|
|
700
|
+
!opts.proxied &&
|
|
701
|
+
isPrivateNetworkAddress(opts.peer));
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* The log line for a source write admitted only by `trustLan`. The file name is the client's,
|
|
705
|
+
* so it is quoted as JSON: a newline or an escape sequence in it cannot forge a log line.
|
|
706
|
+
*/
|
|
707
|
+
export function trustLanWriteRecord(peer, file, status) {
|
|
708
|
+
const name = typeof file === 'string' ? JSON.stringify(file) : '(no file)';
|
|
709
|
+
const outcome = status >= 200 && status < 300 ? 'saved' : `refused (${status})`;
|
|
710
|
+
return `✎ trustLan: ${peer ?? 'unknown'} ${outcome} ${name}`;
|
|
711
|
+
}
|
|
661
712
|
/**
|
|
662
713
|
* May the holder of this session token write?
|
|
663
714
|
*
|
|
@@ -22,7 +22,8 @@ import { resolveTunnelLocalPort } from './site-config.js';
|
|
|
22
22
|
import { acquireBuildLock, describeHolder } from './build-lock.js';
|
|
23
23
|
import { sinkPathFor, appendToSink } from './debug-sink.js';
|
|
24
24
|
import { invitePageHtml } from './invite-page.js';
|
|
25
|
-
import { TUNNEL_LINK_CMD, resolveLinkArrival, isLockedDown, hasTunnel, isLoopbackAddressForAuth as isLoopbackAddress, mayReadSite, shouldInterceptLinkToken, createAuthState, issueLink, resolveShareTtlMs,
|
|
25
|
+
import { TUNNEL_LINK_CMD, resolveLinkArrival, isLockedDown, hasTunnel, isLoopbackAddressForAuth as isLoopbackAddress, mayReadSite, shouldInterceptLinkToken, createAuthState, issueLink, resolveShareTtlMs, readCookie, redeemThroughGate, createRedemptionGate, resolveLinkSettings, sessionCookie, urlWithoutToken, isSameOriginRequest, validSessionCookie, sessionRejection, mayDriveWithAgent, isProxiedRequest, sourceRequestAdmission, suggestsTrustLan, trustLanWriteRecord, SESSION_COOKIE, } from './dev-auth.js';
|
|
26
|
+
import { SOURCE_ACCESS_HEADER, SOURCE_ENDPOINT_SNIPPET, } from '../../live-example/source-access.js';
|
|
26
27
|
/**
|
|
27
28
|
* Every path the dev server watches for changes.
|
|
28
29
|
*
|
|
@@ -842,7 +843,11 @@ export async function devServer(config, opts = {}) {
|
|
|
842
843
|
if (!(await file.exists()))
|
|
843
844
|
return new Response('not found', { status: 404 });
|
|
844
845
|
return new Response(await file.text(), {
|
|
845
|
-
headers: {
|
|
846
|
+
headers: {
|
|
847
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
848
|
+
// Lets a live example off localhost offer Save to source (source-access.ts).
|
|
849
|
+
[SOURCE_ACCESS_HEADER]: 'editable',
|
|
850
|
+
},
|
|
846
851
|
});
|
|
847
852
|
}
|
|
848
853
|
/*
|
|
@@ -1067,7 +1072,11 @@ export async function devServer(config, opts = {}) {
|
|
|
1067
1072
|
? HALTIJA_TUNNEL_SNIPPET
|
|
1068
1073
|
: ''
|
|
1069
1074
|
: HALTIJA_SNIPPET;
|
|
1070
|
-
const extras = haltijaExtra +
|
|
1075
|
+
const extras = haltijaExtra +
|
|
1076
|
+
(testMode ? '' : statusSnippet(request, viaTunnel)) +
|
|
1077
|
+
// Tells a live example there is a source endpoint to ask (source-access.ts), so a
|
|
1078
|
+
// deployed static site, which has none, is never asked.
|
|
1079
|
+
(config.editableSources ? SOURCE_ENDPOINT_SNIPPET : '');
|
|
1071
1080
|
const encoding = negotiateEncoding(request?.headers.get('accept-encoding') ?? null);
|
|
1072
1081
|
if (extras && filePath.endsWith('.html')) {
|
|
1073
1082
|
const html = await Bun.file(filePath).text();
|
|
@@ -1891,28 +1900,27 @@ export async function devServer(config, opts = {}) {
|
|
|
1891
1900
|
"looks local" is precisely what a tunnel counterfeits.
|
|
1892
1901
|
*/
|
|
1893
1902
|
const peer = srv?.requestIP?.(request)?.address;
|
|
1894
|
-
const
|
|
1895
|
-
const authorized = mayWriteSource({
|
|
1903
|
+
const admission = sourceRequestAdmission(request, {
|
|
1896
1904
|
viaTunnel,
|
|
1897
1905
|
peer,
|
|
1898
|
-
|
|
1899
|
-
// A share link's session may browse and may not save (asked AFTER validity, which
|
|
1900
|
-
// is the call-order `cookieMayWrite` documents).
|
|
1901
|
-
sessionMayWrite: cookieMayWrite(auth, session),
|
|
1906
|
+
auth,
|
|
1902
1907
|
trustLan: config.trustLan === true,
|
|
1903
|
-
|
|
1904
|
-
})
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1908
|
+
now: Date.now(),
|
|
1909
|
+
});
|
|
1910
|
+
if (admission === null) {
|
|
1911
|
+
// A HEAD is a page asking whether it may offer Save to source; a no is not news.
|
|
1912
|
+
if (request.method !== 'HEAD') {
|
|
1913
|
+
console.warn(`⚠️ refused ${request.method} /__docstore/source from ${peer ?? 'unknown'} — ` +
|
|
1914
|
+
`no session (use \`${TUNNEL_LINK_CMD}\` for an edit link` +
|
|
1915
|
+
`${suggestsTrustLan({
|
|
1916
|
+
viaTunnel,
|
|
1917
|
+
peer,
|
|
1918
|
+
proxied: isProxiedRequest(request.headers),
|
|
1919
|
+
trustLan: config.trustLan === true,
|
|
1920
|
+
})
|
|
1921
|
+
? '; trustLan: true in the site config admits devices on this network'
|
|
1922
|
+
: ''}).`);
|
|
1923
|
+
}
|
|
1916
1924
|
return new Response('not authorized to edit source', {
|
|
1917
1925
|
status: 403,
|
|
1918
1926
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
@@ -1920,8 +1928,28 @@ export async function devServer(config, opts = {}) {
|
|
|
1920
1928
|
}
|
|
1921
1929
|
if (request.method === 'GET')
|
|
1922
1930
|
return handleReadSource(request);
|
|
1923
|
-
if (request.method === '
|
|
1924
|
-
|
|
1931
|
+
if (request.method === 'HEAD') {
|
|
1932
|
+
const read = await handleReadSource(request);
|
|
1933
|
+
return new Response(null, {
|
|
1934
|
+
status: read.status,
|
|
1935
|
+
headers: read.headers,
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (request.method === 'POST') {
|
|
1939
|
+
// A write admitted only by LAN trust is recorded with its outcome (#3346).
|
|
1940
|
+
const file = admission === 'lan'
|
|
1941
|
+
? await request
|
|
1942
|
+
.clone()
|
|
1943
|
+
.json()
|
|
1944
|
+
.then((body) => body?.file)
|
|
1945
|
+
.catch(() => undefined)
|
|
1946
|
+
: undefined;
|
|
1947
|
+
const response = await handleWriteSource(request);
|
|
1948
|
+
if (admission === 'lan') {
|
|
1949
|
+
console.log(trustLanWriteRecord(peer, file, response.status));
|
|
1950
|
+
}
|
|
1951
|
+
return response;
|
|
1952
|
+
}
|
|
1925
1953
|
return new Response('method not allowed', { status: 405 });
|
|
1926
1954
|
}
|
|
1927
1955
|
/*
|
|
@@ -614,8 +614,9 @@ export interface SiteConfig {
|
|
|
614
614
|
* Trust the local network for reading and editing source (#227). With `editableSources` on,
|
|
615
615
|
* a browser on another device on your LAN (a phone, a headset) can open and save source
|
|
616
616
|
* directly, as one at this keyboard can, instead of needing the tunnel and an invite link.
|
|
617
|
-
* That
|
|
618
|
-
*
|
|
617
|
+
* That covers "Edit page source" and a live example's "Save to source", which is offered
|
|
618
|
+
* wherever the server admits the page. Every write admitted this way is logged with the
|
|
619
|
+
* device's address and the file.
|
|
619
620
|
*
|
|
620
621
|
* Trusted: a direct peer at a private address (RFC 1918 IPv4, IPv6 unique-local or
|
|
621
622
|
* link-local). Never trusted: tunnel traffic (it needs a session as before) or a request
|
package/dist/icon-rules.d.ts
CHANGED
|
@@ -32,5 +32,7 @@ export declare function matchRule(rule: IconRule, name: string): {
|
|
|
32
32
|
baseName: string;
|
|
33
33
|
match: RegExpMatchArray | string;
|
|
34
34
|
} | null;
|
|
35
|
-
/** Whether some rule would draw `name`: it matches, and
|
|
36
|
-
export declare const ruleDraws: (name: string) => boolean;
|
|
35
|
+
/** Whether some rule would draw `name`: it matches, and what it applies to draws (rules chain). */
|
|
36
|
+
export declare const ruleDraws: (name: string, depth?: number) => boolean;
|
|
37
|
+
/** A registered icon, suffixes on one, or a rule that draws (`un:check:square`, `unCheckLock`). */
|
|
38
|
+
export declare const drawable: (name: string, depth?: number) => boolean;
|
package/dist/icon-rules.js
CHANGED
|
@@ -92,8 +92,21 @@ export function matchRule(rule, name) {
|
|
|
92
92
|
match: rule.prefix,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
/*
|
|
96
|
+
How deep rules may chain. Real composites need two (`un:check:square`); the bound is what keeps a
|
|
97
|
+
hostile name cheap. Unbounded, `'un'.repeat(400) + 'Lock'` took over a second, because every
|
|
98
|
+
level asks every rule again (1.16.24 review), and a custom prefix that can match nothing
|
|
99
|
+
(`/^(?:big)?/`) never shortened the name and overflowed the stack.
|
|
100
|
+
*/
|
|
101
|
+
const MAX_RULE_DEPTH = 4;
|
|
102
|
+
/** Whether some rule would draw `name`: it matches, and what it applies to draws (rules chain). */
|
|
103
|
+
export const ruleDraws = (name, depth = 0) => depth < MAX_RULE_DEPTH &&
|
|
104
|
+
iconRules.some((rule) => {
|
|
105
|
+
const matched = matchRule(rule, name);
|
|
106
|
+
return (matched != null &&
|
|
107
|
+
// A rule must shorten the name, or it is not applying to anything.
|
|
108
|
+
matched.baseName.length < name.length &&
|
|
109
|
+
drawable(matched.baseName, depth + 1));
|
|
110
|
+
});
|
|
111
|
+
/** A registered icon, suffixes on one, or a rule that draws (`un:check:square`, `unCheckLock`). */
|
|
112
|
+
export const drawable = (name, depth = 0) => canResolve(name) || ruleDraws(name, depth);
|
package/dist/icons.js
CHANGED
|
@@ -450,7 +450,9 @@ icon. Put a `:` at the boundary to ask for the rule: `check:square`,
|
|
|
450
450
|
`un:lock`, `spin90:loader`. The explicit form applies the rule (unless
|
|
451
451
|
you `defineIcons` that exact name, which warns), needs no capital letter
|
|
452
452
|
after the prefix, and takes suffixes and stacking
|
|
453
|
-
as usual (`spin90:loader40s`, `un:lock$shield`).
|
|
453
|
+
as usual (`spin90:loader40s`, `un:lock$shield`). Rules chain: a rule
|
|
454
|
+
may apply to another rule, so `un:check:square` slashes the checked square
|
|
455
|
+
(and `unCheckLock` works too). A `:` is not valid in a
|
|
454
456
|
JavaScript identifier, so write `icons['check:square']()`, or
|
|
455
457
|
`<tosi-icon icon="check:square">`.
|
|
456
458
|
|
|
@@ -590,7 +592,7 @@ copies or substantial portions of the Software.
|
|
|
590
592
|
import { elements, svgElements, Color, vars, varDefault, withAttributes, } from 'tosijs';
|
|
591
593
|
import iconData from './icon-data.js';
|
|
592
594
|
import { splitSuffixes, suffixNumber } from './icon-suffixes.js';
|
|
593
|
-
import { iconRules, spinRule, matchRule,
|
|
595
|
+
import { iconRules, spinRule, matchRule, drawable, suffixedIcon, } from './icon-rules.js';
|
|
594
596
|
export { iconRules } from './icon-rules.js';
|
|
595
597
|
export const defineIcons = (newIcons) => {
|
|
596
598
|
const data = iconData;
|
|
@@ -608,7 +610,7 @@ export const defineIcons = (newIcons) => {
|
|
|
608
610
|
for (const name of isNew) {
|
|
609
611
|
const readsAsRule = iconRules.some((rule) => {
|
|
610
612
|
const matched = matchRule(rule, name);
|
|
611
|
-
if (matched == null || !
|
|
613
|
+
if (matched == null || !drawable(matched.baseName))
|
|
612
614
|
return false;
|
|
613
615
|
return (typeof rule.prefix !== 'string' ||
|
|
614
616
|
name.includes(':') ||
|
|
@@ -784,8 +786,11 @@ export function wrapIcon(prop, parts, ...children) {
|
|
|
784
786
|
function composeIcon(prop, parts) {
|
|
785
787
|
for (const rule of iconRules) {
|
|
786
788
|
const matched = matchRule(rule, prop);
|
|
787
|
-
// Only apply if baseName
|
|
788
|
-
|
|
789
|
+
// Only apply if baseName draws (an icon, or another rule: `un:check:square`), and the
|
|
790
|
+
// rule shortened the name: a prefix that matched nothing is not applying to anything.
|
|
791
|
+
if (!matched ||
|
|
792
|
+
matched.baseName.length >= prop.length ||
|
|
793
|
+
!drawable(matched.baseName))
|
|
789
794
|
continue;
|
|
790
795
|
const result = rule.apply(safeIconSuffix(matched.baseName), matched.match, parts);
|
|
791
796
|
if (typeof result === 'string')
|