@canmi/me 0.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/LICENSE +7 -0
- package/dist/identity/author.js +16 -0
- package/dist/identity/author.json +13 -0
- package/dist/identity/src/index.d.ts +33 -0
- package/dist/identity/src/index.js +7 -0
- package/dist/locales/src/format.d.ts +35 -0
- package/dist/locales/src/format.js +54 -0
- package/dist/locales/src/index.d.ts +74 -0
- package/dist/locales/src/index.js +104 -0
- package/dist/urls/src/index.d.ts +124 -0
- package/dist/urls/src/index.js +150 -0
- package/dist/urls/src/rust.d.ts +14 -0
- package/dist/urls/src/rust.js +41 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2025 Canmi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region identity/author.json
|
|
2
|
+
var author_default = {
|
|
3
|
+
name: "Canmi",
|
|
4
|
+
fullName: "Canmi Wu",
|
|
5
|
+
role: "Systems Engineer",
|
|
6
|
+
email: "t@canmi.icu",
|
|
7
|
+
telegram: "canmi21",
|
|
8
|
+
telegramGroup: "canmimua",
|
|
9
|
+
twitter: "canmi21",
|
|
10
|
+
github: "canmi21",
|
|
11
|
+
githubId: 72544151,
|
|
12
|
+
fediverse: "canmi",
|
|
13
|
+
bluesky: "canmi.net"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { author_default as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Canmi",
|
|
3
|
+
"fullName": "Canmi Wu",
|
|
4
|
+
"role": "Systems Engineer",
|
|
5
|
+
"email": "t@canmi.icu",
|
|
6
|
+
"telegram": "canmi21",
|
|
7
|
+
"telegramGroup": "canmimua",
|
|
8
|
+
"twitter": "canmi21",
|
|
9
|
+
"github": "canmi21",
|
|
10
|
+
"githubId": 72544151,
|
|
11
|
+
"fediverse": "canmi",
|
|
12
|
+
"bluesky": "canmi.net"
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region identity/src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The author, declared once for every app. The values are a JSON file so that Rust and the
|
|
4
|
+
* scripts outside the Vite graph read the same file. See spec/architecture/identity.md.
|
|
5
|
+
*/
|
|
6
|
+
export interface Author {
|
|
7
|
+
/** The name a page signs with, and the one the author goes by. */
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* Said only where the author is introduced: the home page, its card and its agent view once, and
|
|
11
|
+
* the structured data. Not translated. See spec/architecture/identity.md, "One name, said
|
|
12
|
+
* plainly".
|
|
13
|
+
*/
|
|
14
|
+
fullName: string;
|
|
15
|
+
role: string;
|
|
16
|
+
/** A person's address, and the one the feed names as its author. */
|
|
17
|
+
email: string;
|
|
18
|
+
/** The author's own account. */
|
|
19
|
+
telegram: string;
|
|
20
|
+
/** The group the author runs, which the row of links points at; the handle is theirs too. */
|
|
21
|
+
telegramGroup: string;
|
|
22
|
+
twitter?: string;
|
|
23
|
+
github: string;
|
|
24
|
+
/** GitHub's numeric id, which addresses the avatar and survives a renamed handle. */
|
|
25
|
+
githubId: number;
|
|
26
|
+
fediverse: string;
|
|
27
|
+
/** A domain the owner proves on Bluesky; the site's domain is that proof. */
|
|
28
|
+
bluesky: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const author: Readonly<Author>;
|
|
31
|
+
/** The author named in full anywhere but the home page: the name, then the address. */
|
|
32
|
+
export declare const mailbox: string;
|
|
33
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import author_default from "../author.js";
|
|
2
|
+
//#region identity/src/index.ts
|
|
3
|
+
const author = author_default;
|
|
4
|
+
/** The author named in full anywhere but the home page: the name, then the address. */
|
|
5
|
+
const mailbox = `${author.name} <${author.email}>`;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { author, mailbox };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LocaleCode } from "./index.js";
|
|
2
|
+
//#region locales/src/format.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Formatting the site does the same way everywhere, gathered because it was not.
|
|
5
|
+
*
|
|
6
|
+
* Each of these was written out two to five times, and the copies had begun to disagree in the
|
|
7
|
+
* one way a reader can see: the same magnitude was abbreviated `1.5k` on one widget and `1.5K` on
|
|
8
|
+
* another. Nothing was wrong with either; what was wrong was that the page had two answers.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The tag `Intl` should be given for a view. The source view has no language of its own, so it
|
|
12
|
+
* borrows English for the numbers around its untranslated words. Derived from `PUBLIC_LANGUAGE`
|
|
13
|
+
* rather than restated: the licence pages carried their own copy, four times, spelling `tw` out
|
|
14
|
+
* as a special case -- it produced identical output, which is why nobody noticed the duplication.
|
|
15
|
+
*/
|
|
16
|
+
export declare function intlLocale(locale: LocaleCode): string;
|
|
17
|
+
/**
|
|
18
|
+
* A date as the site writes it: `Apr 13, 2026`.
|
|
19
|
+
*
|
|
20
|
+
* **UTC, always.** The day shown has to match the date in the article's frontmatter, and a page
|
|
21
|
+
* prerendered west of Greenwich would otherwise render the day before. That reason lived beside
|
|
22
|
+
* one of the three copies of this and not the other two.
|
|
23
|
+
*
|
|
24
|
+
* English, also always. These are timestamps on cards and article headers rather than prose, and
|
|
25
|
+
* a translated month name beside an untranslated title reads as a mistake rather than a courtesy.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shortDate(value: string | number | Date): string;
|
|
28
|
+
/**
|
|
29
|
+
* A count shortened to fit a stat row or a chart axis: `950`, `1.5k`, `16k`, `2.3M`. Lowercase
|
|
30
|
+
* `k` and uppercase `M`, which is what SI writes rather than a house style to remember -- the two
|
|
31
|
+
* copies this replaces had picked opposite conventions. One decimal below ten thousand and none
|
|
32
|
+
* above it: past that the tenth is noise, and the shorter label is worth more on an axis.
|
|
33
|
+
*/
|
|
34
|
+
export declare function compactCount(value: number): string;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PUBLIC_LANGUAGE } from "./index.js";
|
|
2
|
+
//#region locales/src/format.ts
|
|
3
|
+
/**
|
|
4
|
+
* Formatting the site does the same way everywhere, gathered because it was not.
|
|
5
|
+
*
|
|
6
|
+
* Each of these was written out two to five times, and the copies had begun to disagree in the
|
|
7
|
+
* one way a reader can see: the same magnitude was abbreviated `1.5k` on one widget and `1.5K` on
|
|
8
|
+
* another. Nothing was wrong with either; what was wrong was that the page had two answers.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The tag `Intl` should be given for a view. The source view has no language of its own, so it
|
|
12
|
+
* borrows English for the numbers around its untranslated words. Derived from `PUBLIC_LANGUAGE`
|
|
13
|
+
* rather than restated: the licence pages carried their own copy, four times, spelling `tw` out
|
|
14
|
+
* as a special case -- it produced identical output, which is why nobody noticed the duplication.
|
|
15
|
+
*/
|
|
16
|
+
function intlLocale(locale) {
|
|
17
|
+
return locale === "mw" ? PUBLIC_LANGUAGE.en : PUBLIC_LANGUAGE[locale];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A date as the site writes it: `Apr 13, 2026`.
|
|
21
|
+
*
|
|
22
|
+
* **UTC, always.** The day shown has to match the date in the article's frontmatter, and a page
|
|
23
|
+
* prerendered west of Greenwich would otherwise render the day before. That reason lived beside
|
|
24
|
+
* one of the three copies of this and not the other two.
|
|
25
|
+
*
|
|
26
|
+
* English, also always. These are timestamps on cards and article headers rather than prose, and
|
|
27
|
+
* a translated month name beside an untranslated title reads as a mistake rather than a courtesy.
|
|
28
|
+
*/
|
|
29
|
+
function shortDate(value) {
|
|
30
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
31
|
+
month: "short",
|
|
32
|
+
day: "numeric",
|
|
33
|
+
year: "numeric",
|
|
34
|
+
timeZone: "UTC"
|
|
35
|
+
}).format(new Date(value));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A count shortened to fit a stat row or a chart axis: `950`, `1.5k`, `16k`, `2.3M`. Lowercase
|
|
39
|
+
* `k` and uppercase `M`, which is what SI writes rather than a house style to remember -- the two
|
|
40
|
+
* copies this replaces had picked opposite conventions. One decimal below ten thousand and none
|
|
41
|
+
* above it: past that the tenth is noise, and the shorter label is worth more on an axis.
|
|
42
|
+
*/
|
|
43
|
+
function compactCount(value) {
|
|
44
|
+
if (value >= 1e6) return `${trim(value / 1e6)}M`;
|
|
45
|
+
if (value >= 1e4) return `${Math.round(value / 1e3)}k`;
|
|
46
|
+
if (value >= 1e3) return `${trim(value / 1e3)}k`;
|
|
47
|
+
return value.toString();
|
|
48
|
+
}
|
|
49
|
+
/** One decimal, and none at all when it would be a zero. */
|
|
50
|
+
function trim(value) {
|
|
51
|
+
return value.toFixed(1).replace(/\.0$/, "");
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { compactCount, intlLocale, shortDate };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region locales/src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What each language calls itself, keyed by the tag the corpus stores.
|
|
4
|
+
*
|
|
5
|
+
* **Never translated, and never a code.** A reader who cannot read the interface still has to find
|
|
6
|
+
* their own language in a list, so the name reads the same whichever view rendered it -- and a
|
|
7
|
+
* writer looking at eight translations of one paragraph should not be reading `ko-KR` to work out
|
|
8
|
+
* which is which. Only the pair that needs telling apart carries a qualifier: there is one English
|
|
9
|
+
* here and two Chinese, and each is written in its own script.
|
|
10
|
+
*
|
|
11
|
+
* The site's language picker was the first consumer and the desktop client's segment view was
|
|
12
|
+
* the second, which is what moved this out of `apps/site/src/lib/locale/switcher.ts`. That client
|
|
13
|
+
* is archived and the surface replacing it inherits the same need, so the move still stands.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LOCALE_TAGS: readonly ['en-US', 'zh-CN', 'ja-JP', 'de-DE', 'ko-KR', 'fr-FR', 'es-ES', 'zh-TW'];
|
|
16
|
+
export type LocaleTag = (typeof LOCALE_TAGS)[number];
|
|
17
|
+
export declare const ENDONYM: {
|
|
18
|
+
readonly 'en-US': 'English';
|
|
19
|
+
readonly 'zh-CN': '中文 (简体)';
|
|
20
|
+
readonly 'zh-TW': '中文 (繁體)';
|
|
21
|
+
readonly 'ja-JP': '日本語';
|
|
22
|
+
readonly 'ko-KR': '한국어';
|
|
23
|
+
readonly 'de-DE': 'Deutsch';
|
|
24
|
+
readonly 'fr-FR': 'Français';
|
|
25
|
+
readonly 'es-ES': 'Español';
|
|
26
|
+
};
|
|
27
|
+
/** The tag's own name, or the tag itself when the corpus grows one this does not know. */
|
|
28
|
+
export declare function endonym(tag: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* The codes the corpus keys a view by, and `mw` for the article's own language.
|
|
31
|
+
*
|
|
32
|
+
* The short internal spelling of the same set [[LOCALE_TAGS]] names in BCP-47. It moved here for
|
|
33
|
+
* the reason those did: a third consumer appeared -- `@monoflake/artifacts`, which types a
|
|
34
|
+
* published view -- and a vocabulary two packages spell out separately is a vocabulary that drifts.
|
|
35
|
+
*/
|
|
36
|
+
export declare const LOCALE_CODES: readonly ['mw', 'de', 'en', 'es', 'fr', 'ja', 'ko', 'zh', 'tw'];
|
|
37
|
+
export type LocaleCode = (typeof LOCALE_CODES)[number];
|
|
38
|
+
/**
|
|
39
|
+
* The language this site's own copy is written in -- not a fallback for a missing article
|
|
40
|
+
* language. It moved here when the API needed to name a locale no article carries a view of,
|
|
41
|
+
* which is the third consumer this table has collected. See lattice's spec/locale/interface.md.
|
|
42
|
+
*/
|
|
43
|
+
export declare const SITE_LANGUAGE = "en-US";
|
|
44
|
+
/**
|
|
45
|
+
* What a code is called in public, which is the only place BCP-47 and the corpus's codes meet.
|
|
46
|
+
*
|
|
47
|
+
* `mw` has no entry: the source view's tag is whatever language the article was written in, so
|
|
48
|
+
* it is the article's to say and not this table's. See lattice's spec/locale/addressing.md.
|
|
49
|
+
*/
|
|
50
|
+
export declare const PUBLIC_LANGUAGE: {
|
|
51
|
+
readonly de: 'de-DE';
|
|
52
|
+
readonly en: 'en-US';
|
|
53
|
+
readonly es: 'es-ES';
|
|
54
|
+
readonly fr: 'fr-FR';
|
|
55
|
+
readonly ja: 'ja-JP';
|
|
56
|
+
readonly ko: 'ko-KR';
|
|
57
|
+
readonly zh: 'zh-CN';
|
|
58
|
+
readonly tw: 'zh-TW';
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The four below are here because the compiler needs them and a request handler does not.
|
|
62
|
+
*
|
|
63
|
+
* What a tag is belongs to this package; what a response does about one -- the cookie, the
|
|
64
|
+
* `Accept-Language` negotiation, the `Vary` -- stays in the application, which is why that half
|
|
65
|
+
* did not move. The boundary was drawn by the second consumer rather than chosen in advance.
|
|
66
|
+
*/
|
|
67
|
+
export declare function localeCode(value: string | null | undefined): LocaleCode | undefined;
|
|
68
|
+
/** The public URL for a selected view; the source view keeps the bare address. */
|
|
69
|
+
export declare function localeUrl(url: string, code: LocaleCode): string;
|
|
70
|
+
/** A public BCP-47 tag. `mw` is the source article, so its tag is article-owned. */
|
|
71
|
+
export declare function languageTag(code: LocaleCode, sourceLanguage: string): string;
|
|
72
|
+
/** Reject malformed source metadata before it reaches `<html lang>` or `og:locale`. */
|
|
73
|
+
export declare function assertLanguageTag(value: unknown, file: string): asserts value is string;
|
|
74
|
+
//#endregion
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region locales/src/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* What each language calls itself, keyed by the tag the corpus stores.
|
|
4
|
+
*
|
|
5
|
+
* **Never translated, and never a code.** A reader who cannot read the interface still has to find
|
|
6
|
+
* their own language in a list, so the name reads the same whichever view rendered it -- and a
|
|
7
|
+
* writer looking at eight translations of one paragraph should not be reading `ko-KR` to work out
|
|
8
|
+
* which is which. Only the pair that needs telling apart carries a qualifier: there is one English
|
|
9
|
+
* here and two Chinese, and each is written in its own script.
|
|
10
|
+
*
|
|
11
|
+
* The site's language picker was the first consumer and the desktop client's segment view was
|
|
12
|
+
* the second, which is what moved this out of `apps/site/src/lib/locale/switcher.ts`. That client
|
|
13
|
+
* is archived and the surface replacing it inherits the same need, so the move still stands.
|
|
14
|
+
*/
|
|
15
|
+
const LOCALE_TAGS = [
|
|
16
|
+
"en-US",
|
|
17
|
+
"zh-CN",
|
|
18
|
+
"ja-JP",
|
|
19
|
+
"de-DE",
|
|
20
|
+
"ko-KR",
|
|
21
|
+
"fr-FR",
|
|
22
|
+
"es-ES",
|
|
23
|
+
"zh-TW"
|
|
24
|
+
];
|
|
25
|
+
const ENDONYM = {
|
|
26
|
+
"en-US": "English",
|
|
27
|
+
"zh-CN": "中文 (简体)",
|
|
28
|
+
"zh-TW": "中文 (繁體)",
|
|
29
|
+
"ja-JP": "日本語",
|
|
30
|
+
"ko-KR": "한국어",
|
|
31
|
+
"de-DE": "Deutsch",
|
|
32
|
+
"fr-FR": "Français",
|
|
33
|
+
"es-ES": "Español"
|
|
34
|
+
};
|
|
35
|
+
/** The tag's own name, or the tag itself when the corpus grows one this does not know. */
|
|
36
|
+
function endonym(tag) {
|
|
37
|
+
return ENDONYM[tag] ?? tag;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The codes the corpus keys a view by, and `mw` for the article's own language.
|
|
41
|
+
*
|
|
42
|
+
* The short internal spelling of the same set [[LOCALE_TAGS]] names in BCP-47. It moved here for
|
|
43
|
+
* the reason those did: a third consumer appeared -- `@monoflake/artifacts`, which types a
|
|
44
|
+
* published view -- and a vocabulary two packages spell out separately is a vocabulary that drifts.
|
|
45
|
+
*/
|
|
46
|
+
const LOCALE_CODES = [
|
|
47
|
+
"mw",
|
|
48
|
+
"de",
|
|
49
|
+
"en",
|
|
50
|
+
"es",
|
|
51
|
+
"fr",
|
|
52
|
+
"ja",
|
|
53
|
+
"ko",
|
|
54
|
+
"zh",
|
|
55
|
+
"tw"
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* The language this site's own copy is written in -- not a fallback for a missing article
|
|
59
|
+
* language. It moved here when the API needed to name a locale no article carries a view of,
|
|
60
|
+
* which is the third consumer this table has collected. See lattice's spec/locale/interface.md.
|
|
61
|
+
*/
|
|
62
|
+
const SITE_LANGUAGE = "en-US";
|
|
63
|
+
/**
|
|
64
|
+
* What a code is called in public, which is the only place BCP-47 and the corpus's codes meet.
|
|
65
|
+
*
|
|
66
|
+
* `mw` has no entry: the source view's tag is whatever language the article was written in, so
|
|
67
|
+
* it is the article's to say and not this table's. See lattice's spec/locale/addressing.md.
|
|
68
|
+
*/
|
|
69
|
+
const PUBLIC_LANGUAGE = {
|
|
70
|
+
de: "de-DE",
|
|
71
|
+
en: "en-US",
|
|
72
|
+
es: "es-ES",
|
|
73
|
+
fr: "fr-FR",
|
|
74
|
+
ja: "ja-JP",
|
|
75
|
+
ko: "ko-KR",
|
|
76
|
+
zh: "zh-CN",
|
|
77
|
+
tw: "zh-TW"
|
|
78
|
+
};
|
|
79
|
+
const CODE_SET = new Set(LOCALE_CODES);
|
|
80
|
+
const LANGUAGE_TAG_SHAPE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
81
|
+
/**
|
|
82
|
+
* The four below are here because the compiler needs them and a request handler does not.
|
|
83
|
+
*
|
|
84
|
+
* What a tag is belongs to this package; what a response does about one -- the cookie, the
|
|
85
|
+
* `Accept-Language` negotiation, the `Vary` -- stays in the application, which is why that half
|
|
86
|
+
* did not move. The boundary was drawn by the second consumer rather than chosen in advance.
|
|
87
|
+
*/
|
|
88
|
+
function localeCode(value) {
|
|
89
|
+
return value != null && CODE_SET.has(value) ? value : void 0;
|
|
90
|
+
}
|
|
91
|
+
/** The public URL for a selected view; the source view keeps the bare address. */
|
|
92
|
+
function localeUrl(url, code) {
|
|
93
|
+
return code === "mw" ? url : `${url}?lang=${code}`;
|
|
94
|
+
}
|
|
95
|
+
/** A public BCP-47 tag. `mw` is the source article, so its tag is article-owned. */
|
|
96
|
+
function languageTag(code, sourceLanguage) {
|
|
97
|
+
return code === "mw" ? sourceLanguage : PUBLIC_LANGUAGE[code];
|
|
98
|
+
}
|
|
99
|
+
/** Reject malformed source metadata before it reaches `<html lang>` or `og:locale`. */
|
|
100
|
+
function assertLanguageTag(value, file) {
|
|
101
|
+
if (typeof value !== "string" || !LANGUAGE_TAG_SHAPE.test(value)) throw new Error(`${file}: invalid BCP-47 lang frontmatter ${JSON.stringify(value)}`);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { ENDONYM, LOCALE_CODES, LOCALE_TAGS, PUBLIC_LANGUAGE, SITE_LANGUAGE, assertLanguageTag, endonym, languageTag, localeCode, localeUrl };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//#region urls/src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The author's own: their site, their identity's addresses, the world's addresses anyone could
|
|
4
|
+
* use, and the functions every program here reads a request with. Depends on nothing else of
|
|
5
|
+
* theirs, so every layer above may read it. See spec/architecture/layers.md, "Addresses are split
|
|
6
|
+
* by who owns the name".
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* How far the sandbox shifts every pinned port, and 0 everywhere else. Read from the environment
|
|
10
|
+
* where there is one, and stated by the build where there is not; production states nothing and
|
|
11
|
+
* gets 0. See spec/architecture/modes.md, "Every port is shifted by one hundred".
|
|
12
|
+
*/
|
|
13
|
+
export declare const PORT_OFFSET: number;
|
|
14
|
+
/** The author's site. */
|
|
15
|
+
export declare const SITE = "https://canmi.net";
|
|
16
|
+
/** The port the site's development server answers on, before the sandbox shifts it. */
|
|
17
|
+
export declare const SITE_PORT = 26511;
|
|
18
|
+
/**
|
|
19
|
+
* The GitHub account everything here is published under.
|
|
20
|
+
*
|
|
21
|
+
* An identity rather than an address, which is why it sits beside the map instead of inside it:
|
|
22
|
+
* the map is mirrored into Rust as URL constants, and this is the one string in it that is not
|
|
23
|
+
* a URL. The CDN's release proxy fetches from this account and no other -- there is no place in
|
|
24
|
+
* its URL to name a different one. See spec/architecture/delivery.md.
|
|
25
|
+
*/
|
|
26
|
+
export declare const GITHUB_OWNER = "canmi21";
|
|
27
|
+
/**
|
|
28
|
+
* Where everything here that is not a dependency comes from. Named at the top of the licence
|
|
29
|
+
* routes, which have to state the terms of the code around the credits as well as the credits
|
|
30
|
+
* themselves -- so it is a published fact, not a convenience, and belongs beside the other URLs
|
|
31
|
+
* rather than written into a route.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SOURCE = "https://github.com/canmi21/lattice";
|
|
34
|
+
/**
|
|
35
|
+
* Where a vulnerability is reported, published in every zone's security.txt. An address on the
|
|
36
|
+
* site's domain that Cloudflare forwards, so the mailbox behind it can change without it.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CONTACT: {
|
|
39
|
+
readonly security: 'mailto:security@canmi.net';
|
|
40
|
+
};
|
|
41
|
+
/** The world's addresses: services somebody else runs, which anything here may reach. */
|
|
42
|
+
export declare const EXTERNAL: {
|
|
43
|
+
readonly github: {
|
|
44
|
+
readonly web: 'https://github.com';
|
|
45
|
+
readonly api: 'https://api.github.com';
|
|
46
|
+
readonly raw: 'https://raw.githubusercontent.com';
|
|
47
|
+
readonly avatars: 'https://avatars.githubusercontent.com';
|
|
48
|
+
readonly cdn: 'https://cdn.jsdelivr.net/gh';
|
|
49
|
+
};
|
|
50
|
+
readonly google: {
|
|
51
|
+
readonly sourcePreferences: 'https://www.google.com/preferences/source';
|
|
52
|
+
};
|
|
53
|
+
readonly registries: {
|
|
54
|
+
readonly npm: 'https://www.npmjs.com';
|
|
55
|
+
readonly cargo: 'https://crates.io';
|
|
56
|
+
readonly cargoIndex: 'https://index.crates.io';
|
|
57
|
+
};
|
|
58
|
+
readonly spdx: 'https://spdx.org/licenses';
|
|
59
|
+
readonly robotstxt: 'https://www.robotstxt.org/robotstxt.html';
|
|
60
|
+
readonly contentSignals: 'https://contentsignals.org';
|
|
61
|
+
readonly contentUsage: 'https://datatracker.ietf.org/doc/draft-ietf-aipref-attach/';
|
|
62
|
+
readonly agentIncident: 'https://openai.com/index/hugging-face-incident-and-the-road-ahead/';
|
|
63
|
+
readonly sentry: {
|
|
64
|
+
readonly site: 'https://a7f2f790ed2fa4f8e0c4310d26d9c39f@o4511131162116096.ingest.us.sentry.io/4511380121976832';
|
|
65
|
+
readonly status: string | undefined;
|
|
66
|
+
};
|
|
67
|
+
readonly feedsmith: 'https://feedsmith.dev';
|
|
68
|
+
readonly indexnow: 'https://api.indexnow.org/IndexNow';
|
|
69
|
+
readonly social: {
|
|
70
|
+
readonly telegram: 'https://t.me';
|
|
71
|
+
readonly twitter: 'https://twitter.com';
|
|
72
|
+
readonly twitterIntent: 'https://twitter.com/intent/follow';
|
|
73
|
+
readonly fediverse: 'https://nya.one';
|
|
74
|
+
readonly bluesky: 'https://bsky.app/profile';
|
|
75
|
+
};
|
|
76
|
+
readonly rust: {
|
|
77
|
+
readonly docs: 'https://docs.rs';
|
|
78
|
+
readonly lib: 'https://lib.rs';
|
|
79
|
+
};
|
|
80
|
+
readonly webring: {
|
|
81
|
+
readonly travellings: 'https://www.travellings.cn/go.html';
|
|
82
|
+
readonly moe: 'https://travel.moe/go?travel=on';
|
|
83
|
+
};
|
|
84
|
+
readonly icpmoe: 'https://icp.gov.moe';
|
|
85
|
+
readonly umami: 'https://cloud.umami.is/script.js';
|
|
86
|
+
readonly umamiGateway: 'https://gateway.umami.is';
|
|
87
|
+
readonly openpanel: 'https://api.openpanel.dev';
|
|
88
|
+
readonly googleFonts: {
|
|
89
|
+
readonly css: 'https://fonts.googleapis.com';
|
|
90
|
+
readonly static: 'https://fonts.gstatic.com';
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The address `local` binds to, and is therefore reached at.
|
|
95
|
+
*
|
|
96
|
+
* A literal rather than `localhost`, because binding is a separate question from addressing:
|
|
97
|
+
* this server binds one address on purpose so it stays off the
|
|
98
|
+
* network, and `localhost` resolves to `::1` first on macOS, which nothing listens on.
|
|
99
|
+
*
|
|
100
|
+
* A bare hostname rather than a URL, because Vite's `server.host` takes the host alone while
|
|
101
|
+
* the Tauri dev URL wants an origin from `loopbackUrl`.
|
|
102
|
+
*/
|
|
103
|
+
export declare const LOOPBACK_HOST = "127.0.0.1";
|
|
104
|
+
export declare function isDevHost(hostname: string): boolean;
|
|
105
|
+
export declare function loopbackUrl(port: number): string;
|
|
106
|
+
/**
|
|
107
|
+
* A request path in its one spelling: every CJK full stop a dot, every backslash a slash, every run
|
|
108
|
+
* of slashes one, and no trailing slash -- so the root alone is `''`, the bare host. See
|
|
109
|
+
* spec/architecture/delivery.md, "Every address has one spelling".
|
|
110
|
+
*/
|
|
111
|
+
export declare function normalizePath(path: string): string;
|
|
112
|
+
/** Where a request belongs and how it is sent there. */
|
|
113
|
+
export type Normalized = {
|
|
114
|
+
location: string;
|
|
115
|
+
status: 301 | 308;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Where a request for `url` belongs, or `undefined` when it is already there. The bare host goes
|
|
119
|
+
* on the wire as `/`, so `/` -- alone or with a query -- is already there, and sending it anywhere
|
|
120
|
+
* would come back as `/` and loop. Any other spelling of the bare host goes to the host itself,
|
|
121
|
+
* no slash, by a 301; any other path to its spelling with the query kept, by a 308.
|
|
122
|
+
*/
|
|
123
|
+
export declare function normalizedLocation(url: URL): Normalized | undefined;
|
|
124
|
+
//#endregion
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region urls/src/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* How far the sandbox shifts every pinned port, and 0 everywhere else. Read from the environment
|
|
4
|
+
* where there is one, and stated by the build where there is not; production states nothing and
|
|
5
|
+
* gets 0. See spec/architecture/modes.md, "Every port is shifted by one hundred".
|
|
6
|
+
*/
|
|
7
|
+
const PORT_OFFSET = typeof STATED_PORT_OFFSET === "number" ? STATED_PORT_OFFSET : Number(globalThis.process?.env?.LATTICE_PORT_OFFSET || 0);
|
|
8
|
+
/** The author's site. */
|
|
9
|
+
const SITE = "https://canmi.net";
|
|
10
|
+
/** The port the site's development server answers on, before the sandbox shifts it. */
|
|
11
|
+
const SITE_PORT = 26511;
|
|
12
|
+
/**
|
|
13
|
+
* The GitHub account everything here is published under.
|
|
14
|
+
*
|
|
15
|
+
* An identity rather than an address, which is why it sits beside the map instead of inside it:
|
|
16
|
+
* the map is mirrored into Rust as URL constants, and this is the one string in it that is not
|
|
17
|
+
* a URL. The CDN's release proxy fetches from this account and no other -- there is no place in
|
|
18
|
+
* its URL to name a different one. See spec/architecture/delivery.md.
|
|
19
|
+
*/
|
|
20
|
+
const GITHUB_OWNER = "canmi21";
|
|
21
|
+
/**
|
|
22
|
+
* Where everything here that is not a dependency comes from. Named at the top of the licence
|
|
23
|
+
* routes, which have to state the terms of the code around the credits as well as the credits
|
|
24
|
+
* themselves -- so it is a published fact, not a convenience, and belongs beside the other URLs
|
|
25
|
+
* rather than written into a route.
|
|
26
|
+
*/
|
|
27
|
+
const SOURCE = `https://github.com/${GITHUB_OWNER}/lattice`;
|
|
28
|
+
/**
|
|
29
|
+
* Where a vulnerability is reported, published in every zone's security.txt. An address on the
|
|
30
|
+
* site's domain that Cloudflare forwards, so the mailbox behind it can change without it.
|
|
31
|
+
*/
|
|
32
|
+
const CONTACT = { security: "mailto:security@canmi.net" };
|
|
33
|
+
/** The world's addresses: services somebody else runs, which anything here may reach. */
|
|
34
|
+
const EXTERNAL = {
|
|
35
|
+
github: {
|
|
36
|
+
web: "https://github.com",
|
|
37
|
+
api: "https://api.github.com",
|
|
38
|
+
raw: "https://raw.githubusercontent.com",
|
|
39
|
+
avatars: "https://avatars.githubusercontent.com",
|
|
40
|
+
cdn: "https://cdn.jsdelivr.net/gh"
|
|
41
|
+
},
|
|
42
|
+
google: { sourcePreferences: "https://www.google.com/preferences/source" },
|
|
43
|
+
registries: {
|
|
44
|
+
npm: "https://www.npmjs.com",
|
|
45
|
+
cargo: "https://crates.io",
|
|
46
|
+
cargoIndex: "https://index.crates.io"
|
|
47
|
+
},
|
|
48
|
+
spdx: "https://spdx.org/licenses",
|
|
49
|
+
robotstxt: "https://www.robotstxt.org/robotstxt.html",
|
|
50
|
+
contentSignals: "https://contentsignals.org",
|
|
51
|
+
contentUsage: "https://datatracker.ietf.org/doc/draft-ietf-aipref-attach/",
|
|
52
|
+
agentIncident: "https://openai.com/index/hugging-face-incident-and-the-road-ahead/",
|
|
53
|
+
sentry: {
|
|
54
|
+
site: "https://a7f2f790ed2fa4f8e0c4310d26d9c39f@o4511131162116096.ingest.us.sentry.io/4511380121976832",
|
|
55
|
+
status: "https://0c9dd7de9a89dddc79dbdc2252e1c940@o4511131162116096.ingest.us.sentry.io/4512173650542592"
|
|
56
|
+
},
|
|
57
|
+
feedsmith: "https://feedsmith.dev",
|
|
58
|
+
indexnow: "https://api.indexnow.org/IndexNow",
|
|
59
|
+
social: {
|
|
60
|
+
telegram: "https://t.me",
|
|
61
|
+
twitter: "https://twitter.com",
|
|
62
|
+
twitterIntent: "https://twitter.com/intent/follow",
|
|
63
|
+
fediverse: "https://nya.one",
|
|
64
|
+
bluesky: "https://bsky.app/profile"
|
|
65
|
+
},
|
|
66
|
+
rust: {
|
|
67
|
+
docs: "https://docs.rs",
|
|
68
|
+
lib: "https://lib.rs"
|
|
69
|
+
},
|
|
70
|
+
webring: {
|
|
71
|
+
travellings: "https://www.travellings.cn/go.html",
|
|
72
|
+
moe: "https://travel.moe/go?travel=on"
|
|
73
|
+
},
|
|
74
|
+
icpmoe: "https://icp.gov.moe",
|
|
75
|
+
umami: "https://cloud.umami.is/script.js",
|
|
76
|
+
umamiGateway: "https://gateway.umami.is",
|
|
77
|
+
openpanel: "https://api.openpanel.dev",
|
|
78
|
+
googleFonts: {
|
|
79
|
+
css: "https://fonts.googleapis.com",
|
|
80
|
+
static: "https://fonts.gstatic.com"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The address `local` binds to, and is therefore reached at.
|
|
85
|
+
*
|
|
86
|
+
* A literal rather than `localhost`, because binding is a separate question from addressing:
|
|
87
|
+
* this server binds one address on purpose so it stays off the
|
|
88
|
+
* network, and `localhost` resolves to `::1` first on macOS, which nothing listens on.
|
|
89
|
+
*
|
|
90
|
+
* A bare hostname rather than a URL, because Vite's `server.host` takes the host alone while
|
|
91
|
+
* the Tauri dev URL wants an origin from `loopbackUrl`.
|
|
92
|
+
*/
|
|
93
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
94
|
+
/**
|
|
95
|
+
* The hostnames that mean this machine.
|
|
96
|
+
*
|
|
97
|
+
* `[::1]` in brackets is the form `URL.hostname` normalises every IPv6 loopback spelling to,
|
|
98
|
+
* and a `Host` header brackets it too; the bare form covers a caller that takes a host apart
|
|
99
|
+
* itself. Without both, a request arriving over IPv6 -- the site binds `::` -- read as production.
|
|
100
|
+
*
|
|
101
|
+
* `LOOPBACK_HOST` covers IPv4: the same address, not the same job as recognising one.
|
|
102
|
+
*/
|
|
103
|
+
const DEV_HOSTS = /* @__PURE__ */ new Set([
|
|
104
|
+
"localhost",
|
|
105
|
+
LOOPBACK_HOST,
|
|
106
|
+
"[::1]",
|
|
107
|
+
"::1"
|
|
108
|
+
]);
|
|
109
|
+
function isDevHost(hostname) {
|
|
110
|
+
return DEV_HOSTS.has(hostname);
|
|
111
|
+
}
|
|
112
|
+
function loopbackUrl(port) {
|
|
113
|
+
const url = new URL(`http://${LOOPBACK_HOST}`);
|
|
114
|
+
url.port = String(port);
|
|
115
|
+
return url.origin;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The full stops a CJK keyboard types for `.` -- ideographic, fullwidth and halfwidth -- the three
|
|
119
|
+
* IDNA already reads as a dot in a hostname, each as written and as a path carries it, encoded.
|
|
120
|
+
*/
|
|
121
|
+
const FULL_STOPS = /[。.。]|%E3%80%82|%EF%BC%8E|%EF%BD%A1/gi;
|
|
122
|
+
/**
|
|
123
|
+
* A request path in its one spelling: every CJK full stop a dot, every backslash a slash, every run
|
|
124
|
+
* of slashes one, and no trailing slash -- so the root alone is `''`, the bare host. See
|
|
125
|
+
* spec/architecture/delivery.md, "Every address has one spelling".
|
|
126
|
+
*/
|
|
127
|
+
function normalizePath(path) {
|
|
128
|
+
return path.replace(FULL_STOPS, ".").replaceAll("\\", "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Where a request for `url` belongs, or `undefined` when it is already there. The bare host goes
|
|
132
|
+
* on the wire as `/`, so `/` -- alone or with a query -- is already there, and sending it anywhere
|
|
133
|
+
* would come back as `/` and loop. Any other spelling of the bare host goes to the host itself,
|
|
134
|
+
* no slash, by a 301; any other path to its spelling with the query kept, by a 308.
|
|
135
|
+
*/
|
|
136
|
+
function normalizedLocation(url) {
|
|
137
|
+
if (url.pathname === "/") return void 0;
|
|
138
|
+
const path = normalizePath(url.pathname);
|
|
139
|
+
if (path === url.pathname) return void 0;
|
|
140
|
+
if (path === "" && url.search === "") return {
|
|
141
|
+
location: url.origin,
|
|
142
|
+
status: 301
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
location: `${path || "/"}${url.search}`,
|
|
146
|
+
status: 308
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { CONTACT, EXTERNAL, GITHUB_OWNER, LOOPBACK_HOST, PORT_OFFSET, SITE, SITE_PORT, SOURCE, isDevHost, loopbackUrl, normalizePath, normalizedLocation };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region urls/src/rust.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Every string under `map` as a Rust `pub const`, named by its path in capitals, so
|
|
4
|
+
* `external.github.web` is `EXTERNAL_GITHUB_WEB`. The generator each address package renders its
|
|
5
|
+
* Rust half with; it knows nothing of what the addresses are. See spec/architecture/layers.md.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rustConstants(map: Record<string, unknown>): string;
|
|
8
|
+
/**
|
|
9
|
+
* This package's Rust half, the `canmi` crate: committed, because a checkout must compile without
|
|
10
|
+
* a Node toolchain having run first. `mise run urls` rewrites it and `rust.test.ts` fails when the
|
|
11
|
+
* two drift.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rustCanmi(): string;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CONTACT, EXTERNAL, SITE, SOURCE } from "./index.js";
|
|
2
|
+
//#region urls/src/rust.ts
|
|
3
|
+
/**
|
|
4
|
+
* Every string under `map` as a Rust `pub const`, named by its path in capitals, so
|
|
5
|
+
* `external.github.web` is `EXTERNAL_GITHUB_WEB`. The generator each address package renders its
|
|
6
|
+
* Rust half with; it knows nothing of what the addresses are. See spec/architecture/layers.md.
|
|
7
|
+
*/
|
|
8
|
+
function rustConstants(map) {
|
|
9
|
+
const pairs = [];
|
|
10
|
+
walk(map, [], pairs);
|
|
11
|
+
return pairs.map(([name, value]) => `pub const ${name}: &str = "${value}";`).join("\n");
|
|
12
|
+
}
|
|
13
|
+
function walk(value, path, out) {
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
const name = path.map((segment) => segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase()).join("_");
|
|
16
|
+
out.push([name, value]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const [key, child] of Object.entries(value)) walk(child, [...path, key], out);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* This package's Rust half, the `canmi` crate: committed, because a checkout must compile without
|
|
23
|
+
* a Node toolchain having run first. `mise run urls` rewrites it and `rust.test.ts` fails when the
|
|
24
|
+
* two drift.
|
|
25
|
+
*/
|
|
26
|
+
function rustCanmi() {
|
|
27
|
+
return [
|
|
28
|
+
"//! @generated from pkgs/me/urls/src/index.ts by `mise run urls`; do not edit.",
|
|
29
|
+
"//! The author's own addresses, for Rust -- see spec/architecture/layers.md.",
|
|
30
|
+
"",
|
|
31
|
+
rustConstants({
|
|
32
|
+
site: SITE,
|
|
33
|
+
source: SOURCE,
|
|
34
|
+
contact: CONTACT,
|
|
35
|
+
external: EXTERNAL
|
|
36
|
+
}),
|
|
37
|
+
""
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { rustCanmi, rustConstants };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canmi/me",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "The author's own facts: their sites' addresses and the world's, their identity, the languages they write in",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/canmi21/lib.git",
|
|
9
|
+
"directory": "pkgs/me"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
"./urls": {
|
|
17
|
+
"types": "./dist/urls/src/index.d.ts",
|
|
18
|
+
"default": "./dist/urls/src/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./rust": {
|
|
21
|
+
"types": "./dist/urls/src/rust.d.ts",
|
|
22
|
+
"default": "./dist/urls/src/rust.js"
|
|
23
|
+
},
|
|
24
|
+
"./identity": {
|
|
25
|
+
"types": "./dist/identity/src/index.d.ts",
|
|
26
|
+
"default": "./dist/identity/src/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./identity/author.json": "./dist/identity/author.json",
|
|
29
|
+
"./locales": {
|
|
30
|
+
"types": "./dist/locales/src/index.d.ts",
|
|
31
|
+
"default": "./dist/locales/src/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./locales/format": {
|
|
34
|
+
"types": "./dist/locales/src/format.d.ts",
|
|
35
|
+
"default": "./dist/locales/src/format.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown"
|
|
40
|
+
}
|
|
41
|
+
}
|