@vp-tw/ptt-link-switcher 0.1.2 → 0.2.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 CHANGED
@@ -13,6 +13,40 @@ if (result.ok) {
13
13
  }
14
14
  ```
15
15
 
16
+ `generateProviderLinks` returns links in provider registry order:
17
+
18
+ ```json
19
+ [
20
+ {
21
+ "id": "official",
22
+ "label": "PTT 官方",
23
+ "url": "https://www.ptt.cc/bbs/Browsers/M.1750772319.A.F2C.html"
24
+ },
25
+ {
26
+ "id": "beptt",
27
+ "label": "BePTT",
28
+ "url": "https://bbs.beptt.tw/Browsers/M.1750772319.A.F2C"
29
+ },
30
+ {
31
+ "id": "pttweb",
32
+ "label": "PTTweb",
33
+ "url": "https://www.pttweb.cc/bbs/Browsers/M.1750772319.A.F2C"
34
+ },
35
+ {
36
+ "id": "moptt",
37
+ "label": "MoPTT",
38
+ "url": "https://moptt.tw/p/Browsers.M.1750772319.A.F2C"
39
+ },
40
+ {
41
+ "id": "nextptt",
42
+ "label": "NextPTT",
43
+ "url": "https://nextptt.app/boards/Browsers/post/M.1750772319.A.F2C"
44
+ }
45
+ ]
46
+ ```
47
+
48
+ The built-in order is Official PTT, BePTT, PTTweb, MoPTT, and NextPTT. This is a default ordering, not a guarantee of social-platform preview compatibility.
49
+
16
50
  Provider support is adapter-based. Pass custom adapters to `parsePttInput` and `generateProviderLinks` to extend parsing and URL generation without changing the built-in registry.
17
51
 
18
52
  The package is framework-free and does not access the DOM, network, or storage.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ interface PttArticleRef {
8
8
  readonly board: string;
9
9
  readonly articleId: string;
10
10
  }
11
- type ProviderId = "official" | "beptt" | "moptt" | "pttweb";
11
+ type ProviderId = "official" | "beptt" | "moptt" | "pttweb" | "nextptt";
12
12
  interface PttUrl {
13
13
  readonly hostname: string;
14
14
  readonly pathname: string;
@@ -73,7 +73,8 @@ declare const officialProvider: ProviderAdapter<"official">;
73
73
  declare const bepttProvider: ProviderAdapter<"beptt">;
74
74
  declare const mopttProvider: ProviderAdapter<"moptt">;
75
75
  declare const pttwebProvider: ProviderAdapter<"pttweb">;
76
- declare const providers: readonly [ProviderAdapter<"official">, ProviderAdapter<"beptt">, ProviderAdapter<"moptt">, ProviderAdapter<"pttweb">];
76
+ declare const nextpttProvider: ProviderAdapter<"nextptt">;
77
+ declare const providers: readonly [ProviderAdapter<"official">, ProviderAdapter<"beptt">, ProviderAdapter<"pttweb">, ProviderAdapter<"moptt">, ProviderAdapter<"nextptt">];
77
78
  declare function getProvider<TAdapter extends ProviderAdapter>(adapters: readonly TAdapter[], id: string): TAdapter | undefined;
78
79
  declare function generateProviderLinks(article: PttArticleRef): ProviderLink<ProviderId>[];
79
80
  declare function generateProviderLinks<TAdapter extends ProviderAdapter>(article: PttArticleRef, adapters: readonly TAdapter[]): ProviderLink<TAdapter["id"]>[];
@@ -82,5 +83,5 @@ declare function generateProviderLinks<TAdapter extends ProviderAdapter>(article
82
83
  declare function normalizeArticleRef(board: string, articleId: string): PttArticleRef | null;
83
84
  declare function isValidBoard(board: string): boolean;
84
85
  //#endregion
85
- export { type ParseError, type ParseOptions, type ParseResult, type ParseSource, type ProviderAdapter, type ProviderId, type ProviderLink, type PttArticleRef, type PttUrl, aidToArticleId, articleIdToAid, bepttProvider, generateProviderLinks, getProvider, isValidAid, isValidBoard, mopttProvider, normalizeArticleRef, officialProvider, parsePttInput, providers, pttwebProvider, tryParsePttInput };
86
+ export { type ParseError, type ParseOptions, type ParseResult, type ParseSource, type ProviderAdapter, type ProviderId, type ProviderLink, type PttArticleRef, type PttUrl, aidToArticleId, articleIdToAid, bepttProvider, generateProviderLinks, getProvider, isValidAid, isValidBoard, mopttProvider, nextpttProvider, normalizeArticleRef, officialProvider, parsePttInput, providers, pttwebProvider, tryParsePttInput };
86
87
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/aid.ts","../src/types.ts","../src/parse.ts","../src/providers.ts","../src/validation.ts"],"mappings":";iBAIgB,WAAW;iBAIX,eAAe;iBAuBf,eAAe;;;UC/Bd;WACN;WACA;;KAGC;UAEK;WACN;WACA;WACA;;UAGM,gBAAgB;WACtB,IAAI;WACJ;WACA;WACA,WAAW,KAAK,WAAW;WAC3B,QAAQ,SAAS;;KAGhB;WACG;;WACA;WAAsB;WAA6B;;KAEtD;WACG;WAA8B;;WAC9B;WAA8B;;WAE9B;WACA;WACA;;WAGA;WACA;WACA;;WAEA;WAAqC;;KAExC;WAEG,SAAS;WACT;WACA,QAAQ;;WAER,OAAO;WAAqB;;UAE1B;WACN;WACA,qBAAqB;;UAGf,aAAa;WACnB,IAAI;WACJ;WACA;;;;iBCXK,cAAc,eAAe,UAAS,eAAoB;iBA8F1D,iBAAiB,eAAe,UAAU,eAAe;;;cCjH5D,kBAAkB;cASlB,eAAe;cAcf,eAAe;cAcf,gBAAgB;cAShB,qBACX,6BACA,0BACA,0BACA;iBAGc,YAAY,iBAAiB,iBAC3C,mBAAmB,YACnB,aACC;iBAIa,sBAAsB,SAAS,gBAAgB,aAAa;iBAC5D,sBAAsB,iBAAiB,iBACrD,SAAS,eACT,mBAAmB,aAClB,aAAa;;;iBCrFA,oBAAoB,eAAe,oBAAoB;iBAgBvD,aAAa"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/aid.ts","../src/types.ts","../src/parse.ts","../src/providers.ts","../src/validation.ts"],"mappings":";iBAIgB,WAAW;iBAIX,eAAe;iBAuBf,eAAe;;;UC/Bd;WACN;WACA;;KAGC;UAEK;WACN;WACA;WACA;;UAGM,gBAAgB;WACtB,IAAI;WACJ;WACA;WACA,WAAW,KAAK,WAAW;WAC3B,QAAQ,SAAS;;KAGhB;WACG;;WACA;WAAsB;WAA6B;;KAEtD;WACG;WAA8B;;WAC9B;WAA8B;;WAE9B;WACA;WACA;;WAGA;WACA;WACA;;WAEA;WAAqC;;KAExC;WAEG,SAAS;WACT;WACA,QAAQ;;WAER,OAAO;WAAqB;;UAE1B;WACN;WACA,qBAAqB;;UAGf,aAAa;WACnB,IAAI;WACJ;WACA;;;;iBCXK,cAAc,eAAe,UAAS,eAAoB;iBA8F1D,iBAAiB,eAAe,UAAU,eAAe;;;cCjH5D,kBAAkB;cASlB,eAAe;cAcf,eAAe;cAcf,gBAAgB;cAShB,iBAAiB;cAgBjB,qBACX,6BACA,0BACA,2BACA,0BACA;iBASc,YAAY,iBAAiB,iBAC3C,mBAAmB,YACnB,aACC;iBAIa,sBAAsB,SAAS,gBAAgB,aAAa;iBAC5D,sBAAsB,iBAAiB,iBACrD,SAAS,eACT,mBAAmB,aAClB,aAAa;;;iBC5GA,oBAAoB,eAAe,oBAAoB;iBAgBvD,aAAa"}
package/dist/index.js CHANGED
@@ -113,11 +113,24 @@ const pttwebProvider = defineProvider({
113
113
  parseUrl: (url) => parsePttStylePath(url.pathname),
114
114
  toUrl: (article) => `https://www.pttweb.cc/bbs/${encodeURIComponent(article.board)}/${article.articleId}`
115
115
  });
116
+ const nextpttProvider = defineProvider({
117
+ hosts: ["nextptt.app"],
118
+ id: "nextptt",
119
+ label: "NextPTT",
120
+ parseUrl: (url) => {
121
+ const match = /^\/boards\/([^/]+)\/post\/((?:M|G)\.\d+\.A(?:\.[0-9A-Fa-f]{3})?)\/?$/.exec(url.pathname);
122
+ if (!match || match[1] === void 0 || match[2] === void 0) return null;
123
+ const board = decodePathPart(match[1]);
124
+ return board === null ? null : normalizeArticleRef(board, match[2]);
125
+ },
126
+ toUrl: (article) => `https://nextptt.app/boards/${encodeURIComponent(article.board)}/post/${article.articleId}`
127
+ });
116
128
  const providers = Object.freeze([
117
129
  officialProvider,
118
130
  bepttProvider,
131
+ pttwebProvider,
119
132
  mopttProvider,
120
- pttwebProvider
133
+ nextpttProvider
121
134
  ]);
122
135
  function getProvider(adapters, id) {
123
136
  return adapters.find((adapter) => adapter.id === id);
@@ -236,6 +249,6 @@ function tryParsePttInput(input, options) {
236
249
  return result.ok ? result.article : null;
237
250
  }
238
251
  //#endregion
239
- export { aidToArticleId, articleIdToAid, bepttProvider, generateProviderLinks, getProvider, isValidAid, isValidBoard, mopttProvider, normalizeArticleRef, officialProvider, parsePttInput, providers, pttwebProvider, tryParsePttInput };
252
+ export { aidToArticleId, articleIdToAid, bepttProvider, generateProviderLinks, getProvider, isValidAid, isValidBoard, mopttProvider, nextpttProvider, normalizeArticleRef, officialProvider, parsePttInput, providers, pttwebProvider, tryParsePttInput };
240
253
 
241
254
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ARTICLE_ID_PATTERN","defaultProviders"],"sources":["../src/aid.ts","../src/validation.ts","../src/providers.ts","../src/parse.ts"],"sourcesContent":["const AID_ALPHABET = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_\";\nconst AID_PATTERN = /^[0-9A-Za-z_-]{8}$/;\nconst ARTICLE_ID_PATTERN = /^(M|G)\\.(\\d+)\\.A(?:\\.([0-9A-F]{3}))?$/;\n\nexport function isValidAid(value: string): boolean {\n return AID_PATTERN.test(value);\n}\n\nexport function aidToArticleId(aid: string): string | null {\n const normalized = aid.startsWith(\"#\") ? aid.slice(1) : aid;\n if (!isValidAid(normalized)) return null;\n\n let timestamp = 0;\n for (const character of normalized.slice(0, 6)) {\n const index = AID_ALPHABET.indexOf(character);\n if (index < 0) return null;\n timestamp = timestamp * 64 + index;\n }\n\n let checksum = 0;\n for (const character of normalized.slice(6)) {\n const index = AID_ALPHABET.indexOf(character);\n if (index < 0) return null;\n checksum = checksum * 64 + index;\n }\n\n const checksumPart =\n checksum === 0 ? \"\" : `.${checksum.toString(16).toUpperCase().padStart(3, \"0\")}`;\n return `M.${timestamp}.A${checksumPart}`;\n}\n\nexport function articleIdToAid(articleId: string): string | null {\n const match = ARTICLE_ID_PATTERN.exec(articleId.toUpperCase());\n if (!match || match[1] !== \"M\" || match[2] === undefined) return null;\n\n let timestamp = Number(match[2]);\n let checksum = match[3] === undefined ? 0 : Number.parseInt(match[3], 16);\n if (!Number.isSafeInteger(timestamp) || timestamp < 0 || checksum > 0xfff) return null;\n\n let encodedTimestamp = \"\";\n for (let index = 0; index < 6; index += 1) {\n encodedTimestamp = AID_ALPHABET[timestamp % 64]! + encodedTimestamp;\n timestamp = Math.floor(timestamp / 64);\n }\n\n let encodedChecksum = \"\";\n for (let index = 0; index < 2; index += 1) {\n encodedChecksum = AID_ALPHABET[checksum % 64]! + encodedChecksum;\n checksum = Math.floor(checksum / 64);\n }\n\n return `#${encodedTimestamp}${encodedChecksum}`;\n}\n","import type { PttArticleRef } from \"./types.js\";\n\nconst BOARD_PATTERN = /^[A-Za-z0-9_.-]+$/;\nconst ARTICLE_ID_PATTERN = /^(M|G)\\.\\d+\\.A(?:\\.[0-9A-F]{3})?$/;\n\nexport function normalizeArticleRef(board: string, articleId: string): PttArticleRef | null {\n const normalizedBoard = board.trim();\n const normalizedArticleId = articleId.trim().toUpperCase();\n\n if (\n normalizedBoard.length === 0 ||\n normalizedBoard.length > 64 ||\n !BOARD_PATTERN.test(normalizedBoard) ||\n !ARTICLE_ID_PATTERN.test(normalizedArticleId)\n ) {\n return null;\n }\n\n return { articleId: normalizedArticleId, board: normalizedBoard };\n}\n\nexport function isValidBoard(board: string): boolean {\n return normalizeArticleRef(board, \"M.0.A\") !== null;\n}\n","import type { ProviderAdapter, ProviderId, ProviderLink, PttArticleRef, PttUrl } from \"./types.js\";\nimport { normalizeArticleRef } from \"./validation.js\";\n\nfunction decodePathPart(value: string): string | null {\n try {\n return decodeURIComponent(value);\n } catch {\n return null;\n }\n}\n\nfunction parsePttStylePath(pathname: string): PttArticleRef | null {\n const match = /^\\/bbs\\/([^/]+)\\/((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)(?:\\.html)?\\/?$/.exec(\n pathname,\n );\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n}\n\nfunction defineProvider<TId extends ProviderId>(\n provider: ProviderAdapter<TId>,\n): ProviderAdapter<TId> {\n return Object.freeze(provider);\n}\n\nexport const officialProvider: ProviderAdapter<\"official\"> = defineProvider({\n hosts: [\"www.ptt.cc\", \"ptt.cc\"],\n id: \"official\",\n label: \"PTT 官方\",\n parseUrl: (url: PttUrl): PttArticleRef | null => parsePttStylePath(url.pathname),\n toUrl: (article: PttArticleRef): string =>\n `https://www.ptt.cc/bbs/${encodeURIComponent(article.board)}/${article.articleId}.html`,\n});\n\nexport const bepttProvider: ProviderAdapter<\"beptt\"> = defineProvider({\n hosts: [\"bbs.beptt.tw\", \"bbs.beptt.cc\"],\n id: \"beptt\",\n label: \"BePTT\",\n parseUrl: (url: PttUrl): PttArticleRef | null => {\n const match = /^\\/([^/]+)\\/((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)\\/?$/.exec(url.pathname);\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n },\n toUrl: (article: PttArticleRef): string =>\n `https://bbs.beptt.tw/${encodeURIComponent(article.board)}/${article.articleId}`,\n});\n\nexport const mopttProvider: ProviderAdapter<\"moptt\"> = defineProvider({\n hosts: [\"moptt.tw\", \"www.moptt.tw\"],\n id: \"moptt\",\n label: \"MoPTT\",\n parseUrl: (url: PttUrl): PttArticleRef | null => {\n const match = /^\\/p\\/(.+?)\\.((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)\\/?$/.exec(url.pathname);\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n },\n toUrl: (article: PttArticleRef): string =>\n `https://moptt.tw/p/${encodeURIComponent(article.board)}.${article.articleId}`,\n});\n\nexport const pttwebProvider: ProviderAdapter<\"pttweb\"> = defineProvider({\n hosts: [\"www.pttweb.cc\", \"pttweb.cc\"],\n id: \"pttweb\",\n label: \"PTTweb\",\n parseUrl: (url: PttUrl): PttArticleRef | null => parsePttStylePath(url.pathname),\n toUrl: (article: PttArticleRef): string =>\n `https://www.pttweb.cc/bbs/${encodeURIComponent(article.board)}/${article.articleId}`,\n});\n\nexport const providers: readonly [\n ProviderAdapter<\"official\">,\n ProviderAdapter<\"beptt\">,\n ProviderAdapter<\"moptt\">,\n ProviderAdapter<\"pttweb\">,\n] = Object.freeze([officialProvider, bepttProvider, mopttProvider, pttwebProvider] as const);\n\nexport function getProvider<TAdapter extends ProviderAdapter>(\n adapters: readonly TAdapter[],\n id: string,\n): TAdapter | undefined {\n return adapters.find((adapter) => adapter.id === id);\n}\n\nexport function generateProviderLinks(article: PttArticleRef): ProviderLink<ProviderId>[];\nexport function generateProviderLinks<TAdapter extends ProviderAdapter>(\n article: PttArticleRef,\n adapters: readonly TAdapter[],\n): ProviderLink<TAdapter[\"id\"]>[];\nexport function generateProviderLinks(\n article: PttArticleRef,\n adapters: readonly ProviderAdapter[] = providers,\n): ProviderLink[] {\n return adapters.map((adapter) => ({\n id: adapter.id,\n label: adapter.label,\n url: adapter.toUrl(article),\n }));\n}\n","import { aidToArticleId, isValidAid } from \"./aid.js\";\nimport { providers as defaultProviders } from \"./providers.js\";\nimport type { ParseOptions, ParseResult, ProviderAdapter, PttArticleRef } from \"./types.js\";\nimport { isValidBoard, normalizeArticleRef } from \"./validation.js\";\n\nconst URL_PATTERN = /https?:\\/\\/[^\\s<>\"']+/giu;\nconst AID_INPUT_PATTERN = /^#?([0-9A-Za-z_-]{8})(?:\\s*\\(([^)]+)\\))?$/;\nconst TRAILING_PUNCTUATION = /[),.!?,。!?;;::]+$/u;\n\nfunction urlsFromText(input: string): URL[] {\n const matches = input.match(URL_PATTERN) ?? [];\n const urls: URL[] = [];\n\n for (const match of matches) {\n try {\n urls.push(new URL(match.replace(TRAILING_PUNCTUATION, \"\")));\n } catch {\n // Ignore malformed URL-like fragments and continue to AID parsing.\n }\n }\n\n return urls;\n}\n\nfunction adapterForUrl(\n url: URL,\n adapters: readonly ProviderAdapter[],\n): ProviderAdapter | undefined {\n const hostname = url.hostname.toLowerCase();\n return adapters.find((adapter) => adapter.hosts.includes(hostname));\n}\n\nfunction success(\n article: PttArticleRef,\n source:\n | { readonly kind: \"aid\" }\n | {\n readonly kind: \"url\";\n readonly providerId: string;\n readonly url: string;\n },\n): ParseResult {\n return { article, ok: true, source };\n}\n\nexport function parsePttInput(input: string, options: ParseOptions = {}): ParseResult {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n return {\n error: { code: \"empty_input\", message: \"Enter a PTT article URL or full AID.\" },\n ok: false,\n };\n }\n\n const adapters = options.providers ?? defaultProviders;\n const urls = urlsFromText(trimmed);\n let unsupportedHost: string | undefined;\n\n for (const url of urls) {\n const adapter = adapterForUrl(url, adapters);\n if (adapter === undefined) {\n unsupportedHost ??= url.hostname;\n continue;\n }\n\n const article = adapter.parseUrl(url);\n if (article !== null) {\n return success(article, {\n kind: \"url\",\n providerId: adapter.id,\n url: url.toString(),\n });\n }\n }\n\n const aidMatch = AID_INPUT_PATTERN.exec(trimmed);\n if (aidMatch?.[1] !== undefined) {\n const aid = aidMatch[1];\n if (!isValidAid(aid)) {\n return {\n error: { code: \"invalid_aid\", message: \"The AID must contain exactly 8 characters.\" },\n ok: false,\n };\n }\n\n const board = (aidMatch[2] ?? options.board)?.trim();\n if (board === undefined || board.length === 0) {\n return {\n error: {\n aid: `#${aid}`,\n code: \"missing_board\",\n message: \"Add the board name to convert this AID offline.\",\n },\n ok: false,\n };\n }\n\n if (!isValidBoard(board)) {\n return {\n error: { code: \"invalid_aid\", message: \"The board name is not valid.\" },\n ok: false,\n };\n }\n\n const articleId = aidToArticleId(aid);\n const article = articleId === null ? null : normalizeArticleRef(board, articleId);\n if (article !== null) return success(article, { kind: \"aid\" });\n }\n\n if (trimmed.startsWith(\"#\")) {\n return {\n error: {\n code: \"invalid_aid\",\n message: \"The AID must contain exactly 8 supported characters.\",\n },\n ok: false,\n };\n }\n\n if (unsupportedHost !== undefined) {\n return {\n error: {\n code: \"unsupported_provider\",\n host: unsupportedHost,\n message: `Unsupported PTT reader: ${unsupportedHost}`,\n },\n ok: false,\n };\n }\n\n return {\n error: {\n code: \"unrecognized_input\",\n message: \"No supported PTT article URL or full AID was found.\",\n },\n ok: false,\n };\n}\n\nexport function tryParsePttInput(input: string, options?: ParseOptions): PttArticleRef | null {\n const result = parsePttInput(input, options);\n return result.ok ? result.article : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["ARTICLE_ID_PATTERN","defaultProviders"],"sources":["../src/aid.ts","../src/validation.ts","../src/providers.ts","../src/parse.ts"],"sourcesContent":["const AID_ALPHABET = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_\";\nconst AID_PATTERN = /^[0-9A-Za-z_-]{8}$/;\nconst ARTICLE_ID_PATTERN = /^(M|G)\\.(\\d+)\\.A(?:\\.([0-9A-F]{3}))?$/;\n\nexport function isValidAid(value: string): boolean {\n return AID_PATTERN.test(value);\n}\n\nexport function aidToArticleId(aid: string): string | null {\n const normalized = aid.startsWith(\"#\") ? aid.slice(1) : aid;\n if (!isValidAid(normalized)) return null;\n\n let timestamp = 0;\n for (const character of normalized.slice(0, 6)) {\n const index = AID_ALPHABET.indexOf(character);\n if (index < 0) return null;\n timestamp = timestamp * 64 + index;\n }\n\n let checksum = 0;\n for (const character of normalized.slice(6)) {\n const index = AID_ALPHABET.indexOf(character);\n if (index < 0) return null;\n checksum = checksum * 64 + index;\n }\n\n const checksumPart =\n checksum === 0 ? \"\" : `.${checksum.toString(16).toUpperCase().padStart(3, \"0\")}`;\n return `M.${timestamp}.A${checksumPart}`;\n}\n\nexport function articleIdToAid(articleId: string): string | null {\n const match = ARTICLE_ID_PATTERN.exec(articleId.toUpperCase());\n if (!match || match[1] !== \"M\" || match[2] === undefined) return null;\n\n let timestamp = Number(match[2]);\n let checksum = match[3] === undefined ? 0 : Number.parseInt(match[3], 16);\n if (!Number.isSafeInteger(timestamp) || timestamp < 0 || checksum > 0xfff) return null;\n\n let encodedTimestamp = \"\";\n for (let index = 0; index < 6; index += 1) {\n encodedTimestamp = AID_ALPHABET[timestamp % 64]! + encodedTimestamp;\n timestamp = Math.floor(timestamp / 64);\n }\n\n let encodedChecksum = \"\";\n for (let index = 0; index < 2; index += 1) {\n encodedChecksum = AID_ALPHABET[checksum % 64]! + encodedChecksum;\n checksum = Math.floor(checksum / 64);\n }\n\n return `#${encodedTimestamp}${encodedChecksum}`;\n}\n","import type { PttArticleRef } from \"./types.js\";\n\nconst BOARD_PATTERN = /^[A-Za-z0-9_.-]+$/;\nconst ARTICLE_ID_PATTERN = /^(M|G)\\.\\d+\\.A(?:\\.[0-9A-F]{3})?$/;\n\nexport function normalizeArticleRef(board: string, articleId: string): PttArticleRef | null {\n const normalizedBoard = board.trim();\n const normalizedArticleId = articleId.trim().toUpperCase();\n\n if (\n normalizedBoard.length === 0 ||\n normalizedBoard.length > 64 ||\n !BOARD_PATTERN.test(normalizedBoard) ||\n !ARTICLE_ID_PATTERN.test(normalizedArticleId)\n ) {\n return null;\n }\n\n return { articleId: normalizedArticleId, board: normalizedBoard };\n}\n\nexport function isValidBoard(board: string): boolean {\n return normalizeArticleRef(board, \"M.0.A\") !== null;\n}\n","import type { ProviderAdapter, ProviderId, ProviderLink, PttArticleRef, PttUrl } from \"./types.js\";\nimport { normalizeArticleRef } from \"./validation.js\";\n\nfunction decodePathPart(value: string): string | null {\n try {\n return decodeURIComponent(value);\n } catch {\n return null;\n }\n}\n\nfunction parsePttStylePath(pathname: string): PttArticleRef | null {\n const match = /^\\/bbs\\/([^/]+)\\/((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)(?:\\.html)?\\/?$/.exec(\n pathname,\n );\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n}\n\nfunction defineProvider<TId extends ProviderId>(\n provider: ProviderAdapter<TId>,\n): ProviderAdapter<TId> {\n return Object.freeze(provider);\n}\n\nexport const officialProvider: ProviderAdapter<\"official\"> = defineProvider({\n hosts: [\"www.ptt.cc\", \"ptt.cc\"],\n id: \"official\",\n label: \"PTT 官方\",\n parseUrl: (url: PttUrl): PttArticleRef | null => parsePttStylePath(url.pathname),\n toUrl: (article: PttArticleRef): string =>\n `https://www.ptt.cc/bbs/${encodeURIComponent(article.board)}/${article.articleId}.html`,\n});\n\nexport const bepttProvider: ProviderAdapter<\"beptt\"> = defineProvider({\n hosts: [\"bbs.beptt.tw\", \"bbs.beptt.cc\"],\n id: \"beptt\",\n label: \"BePTT\",\n parseUrl: (url: PttUrl): PttArticleRef | null => {\n const match = /^\\/([^/]+)\\/((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)\\/?$/.exec(url.pathname);\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n },\n toUrl: (article: PttArticleRef): string =>\n `https://bbs.beptt.tw/${encodeURIComponent(article.board)}/${article.articleId}`,\n});\n\nexport const mopttProvider: ProviderAdapter<\"moptt\"> = defineProvider({\n hosts: [\"moptt.tw\", \"www.moptt.tw\"],\n id: \"moptt\",\n label: \"MoPTT\",\n parseUrl: (url: PttUrl): PttArticleRef | null => {\n const match = /^\\/p\\/(.+?)\\.((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)\\/?$/.exec(url.pathname);\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n },\n toUrl: (article: PttArticleRef): string =>\n `https://moptt.tw/p/${encodeURIComponent(article.board)}.${article.articleId}`,\n});\n\nexport const pttwebProvider: ProviderAdapter<\"pttweb\"> = defineProvider({\n hosts: [\"www.pttweb.cc\", \"pttweb.cc\"],\n id: \"pttweb\",\n label: \"PTTweb\",\n parseUrl: (url: PttUrl): PttArticleRef | null => parsePttStylePath(url.pathname),\n toUrl: (article: PttArticleRef): string =>\n `https://www.pttweb.cc/bbs/${encodeURIComponent(article.board)}/${article.articleId}`,\n});\n\nexport const nextpttProvider: ProviderAdapter<\"nextptt\"> = defineProvider({\n hosts: [\"nextptt.app\"],\n id: \"nextptt\",\n label: \"NextPTT\",\n parseUrl: (url: PttUrl): PttArticleRef | null => {\n const match = /^\\/boards\\/([^/]+)\\/post\\/((?:M|G)\\.\\d+\\.A(?:\\.[0-9A-Fa-f]{3})?)\\/?$/.exec(\n url.pathname,\n );\n if (!match || match[1] === undefined || match[2] === undefined) return null;\n const board = decodePathPart(match[1]);\n return board === null ? null : normalizeArticleRef(board, match[2]);\n },\n toUrl: (article: PttArticleRef): string =>\n `https://nextptt.app/boards/${encodeURIComponent(article.board)}/post/${article.articleId}`,\n});\n\nexport const providers: readonly [\n ProviderAdapter<\"official\">,\n ProviderAdapter<\"beptt\">,\n ProviderAdapter<\"pttweb\">,\n ProviderAdapter<\"moptt\">,\n ProviderAdapter<\"nextptt\">,\n] = Object.freeze([\n officialProvider,\n bepttProvider,\n pttwebProvider,\n mopttProvider,\n nextpttProvider,\n] as const);\n\nexport function getProvider<TAdapter extends ProviderAdapter>(\n adapters: readonly TAdapter[],\n id: string,\n): TAdapter | undefined {\n return adapters.find((adapter) => adapter.id === id);\n}\n\nexport function generateProviderLinks(article: PttArticleRef): ProviderLink<ProviderId>[];\nexport function generateProviderLinks<TAdapter extends ProviderAdapter>(\n article: PttArticleRef,\n adapters: readonly TAdapter[],\n): ProviderLink<TAdapter[\"id\"]>[];\nexport function generateProviderLinks(\n article: PttArticleRef,\n adapters: readonly ProviderAdapter[] = providers,\n): ProviderLink[] {\n return adapters.map((adapter) => ({\n id: adapter.id,\n label: adapter.label,\n url: adapter.toUrl(article),\n }));\n}\n","import { aidToArticleId, isValidAid } from \"./aid.js\";\nimport { providers as defaultProviders } from \"./providers.js\";\nimport type { ParseOptions, ParseResult, ProviderAdapter, PttArticleRef } from \"./types.js\";\nimport { isValidBoard, normalizeArticleRef } from \"./validation.js\";\n\nconst URL_PATTERN = /https?:\\/\\/[^\\s<>\"']+/giu;\nconst AID_INPUT_PATTERN = /^#?([0-9A-Za-z_-]{8})(?:\\s*\\(([^)]+)\\))?$/;\nconst TRAILING_PUNCTUATION = /[),.!?,。!?;;::]+$/u;\n\nfunction urlsFromText(input: string): URL[] {\n const matches = input.match(URL_PATTERN) ?? [];\n const urls: URL[] = [];\n\n for (const match of matches) {\n try {\n urls.push(new URL(match.replace(TRAILING_PUNCTUATION, \"\")));\n } catch {\n // Ignore malformed URL-like fragments and continue to AID parsing.\n }\n }\n\n return urls;\n}\n\nfunction adapterForUrl(\n url: URL,\n adapters: readonly ProviderAdapter[],\n): ProviderAdapter | undefined {\n const hostname = url.hostname.toLowerCase();\n return adapters.find((adapter) => adapter.hosts.includes(hostname));\n}\n\nfunction success(\n article: PttArticleRef,\n source:\n | { readonly kind: \"aid\" }\n | {\n readonly kind: \"url\";\n readonly providerId: string;\n readonly url: string;\n },\n): ParseResult {\n return { article, ok: true, source };\n}\n\nexport function parsePttInput(input: string, options: ParseOptions = {}): ParseResult {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n return {\n error: { code: \"empty_input\", message: \"Enter a PTT article URL or full AID.\" },\n ok: false,\n };\n }\n\n const adapters = options.providers ?? defaultProviders;\n const urls = urlsFromText(trimmed);\n let unsupportedHost: string | undefined;\n\n for (const url of urls) {\n const adapter = adapterForUrl(url, adapters);\n if (adapter === undefined) {\n unsupportedHost ??= url.hostname;\n continue;\n }\n\n const article = adapter.parseUrl(url);\n if (article !== null) {\n return success(article, {\n kind: \"url\",\n providerId: adapter.id,\n url: url.toString(),\n });\n }\n }\n\n const aidMatch = AID_INPUT_PATTERN.exec(trimmed);\n if (aidMatch?.[1] !== undefined) {\n const aid = aidMatch[1];\n if (!isValidAid(aid)) {\n return {\n error: { code: \"invalid_aid\", message: \"The AID must contain exactly 8 characters.\" },\n ok: false,\n };\n }\n\n const board = (aidMatch[2] ?? options.board)?.trim();\n if (board === undefined || board.length === 0) {\n return {\n error: {\n aid: `#${aid}`,\n code: \"missing_board\",\n message: \"Add the board name to convert this AID offline.\",\n },\n ok: false,\n };\n }\n\n if (!isValidBoard(board)) {\n return {\n error: { code: \"invalid_aid\", message: \"The board name is not valid.\" },\n ok: false,\n };\n }\n\n const articleId = aidToArticleId(aid);\n const article = articleId === null ? null : normalizeArticleRef(board, articleId);\n if (article !== null) return success(article, { kind: \"aid\" });\n }\n\n if (trimmed.startsWith(\"#\")) {\n return {\n error: {\n code: \"invalid_aid\",\n message: \"The AID must contain exactly 8 supported characters.\",\n },\n ok: false,\n };\n }\n\n if (unsupportedHost !== undefined) {\n return {\n error: {\n code: \"unsupported_provider\",\n host: unsupportedHost,\n message: `Unsupported PTT reader: ${unsupportedHost}`,\n },\n ok: false,\n };\n }\n\n return {\n error: {\n code: \"unrecognized_input\",\n message: \"No supported PTT article URL or full AID was found.\",\n },\n ok: false,\n };\n}\n\nexport function tryParsePttInput(input: string, options?: ParseOptions): PttArticleRef | null {\n const result = parsePttInput(input, options);\n return result.ok ? result.article : null;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vp-tw/ptt-link-switcher",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Parse and convert PTT article links across supported readers—entirely offline.",
5
5
  "license": "MIT",
6
6
  "repository": {