@khorsheed/dsh-reader 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.
Files changed (58) hide show
  1. package/README.en.md +90 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +90 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +15119 -0
  6. package/lib/index.js +3660 -0
  7. package/lib/invariant.js +38 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1738 -0
  10. package/lib/typert.remote-client.d.ts +82 -0
  11. package/lib/typert.remote-client.js +1115 -0
  12. package/lib/types/arxiv.d.ts +47 -0
  13. package/lib/types/arxiv.js +76 -0
  14. package/lib/types/client/ReaderPane.d.ts +71 -0
  15. package/lib/types/client/ReaderPane.js +3135 -0
  16. package/lib/types/client/contract.d.ts +254 -0
  17. package/lib/types/client/contract.js +1 -0
  18. package/lib/types/client/definition.d.ts +27 -0
  19. package/lib/types/client/definition.js +32 -0
  20. package/lib/types/client/extract-article.d.ts +102 -0
  21. package/lib/types/client/extract-article.js +1024 -0
  22. package/lib/types/client/icons.d.ts +27 -0
  23. package/lib/types/client/icons.js +11 -0
  24. package/lib/types/client/index.d.ts +34 -0
  25. package/lib/types/client/index.js +222 -0
  26. package/lib/types/client/locales.d.ts +391 -0
  27. package/lib/types/client/locales.js +382 -0
  28. package/lib/types/client/parse-rss.d.ts +69 -0
  29. package/lib/types/client/parse-rss.js +656 -0
  30. package/lib/types/client/quote.d.ts +89 -0
  31. package/lib/types/client/quote.js +119 -0
  32. package/lib/types/client/selectors.d.ts +167 -0
  33. package/lib/types/client/selectors.js +413 -0
  34. package/lib/types/client/session.d.ts +171 -0
  35. package/lib/types/client/session.js +223 -0
  36. package/lib/types/client/store.d.ts +181 -0
  37. package/lib/types/client/store.js +226 -0
  38. package/lib/types/client/translate.d.ts +379 -0
  39. package/lib/types/client/translate.js +873 -0
  40. package/lib/types/index.d.ts +48 -0
  41. package/lib/types/index.js +25 -0
  42. package/lib/types/invariant.d.ts +29 -0
  43. package/lib/types/invariant.js +38 -0
  44. package/lib/types/link-resolvers.d.ts +107 -0
  45. package/lib/types/link-resolvers.js +299 -0
  46. package/lib/types/remote.d.ts +224 -0
  47. package/lib/types/remote.js +275 -0
  48. package/lib/types/schedule.d.ts +78 -0
  49. package/lib/types/schedule.js +114 -0
  50. package/lib/types/service.d.ts +633 -0
  51. package/lib/types/service.js +1697 -0
  52. package/lib/types/store.d.ts +224 -0
  53. package/lib/types/store.js +916 -0
  54. package/lib/types/types.d.ts +710 -0
  55. package/lib/types/types.js +182 -0
  56. package/locale/en.json +6 -0
  57. package/locale/zh.json +6 -0
  58. package/package.json +128 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Compile-time contract between the reader's browser half and its host half.
3
+ *
4
+ * Nothing here executes: this module holds the shapes only, so a change in the
5
+ * Remote surface or in the injected business face breaks compilation at the
6
+ * components that consume them instead of at runtime.
7
+ *
8
+ * @module @khorsheed/dsh-reader/client/contract
9
+ */
10
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
11
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
12
+ import type { GlobalStandardProps, InjectFace, PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
13
+ import type { ReaderAddFailure, ReaderAddOutcome, ReaderAddRefusal, ReaderAnnotationOutcome, ReaderBackfillCandidate, ReaderEntryBodyView, ReaderEntryFetchState, ReaderBody, ReaderCapabilities, ReaderEntryTranslationView, ReaderMutationOutcome, ReaderRefreshResult, ReaderRecentEntry, ReaderSentenceLearn, ReaderSourceSummary, ReaderStorageStats, ReaderTag } from '../types.ts';
14
+ import type { createReaderStore } from './store.ts';
15
+ export type { RemoteResult };
16
+ /** The result of extracting one article, as the detail view consumes it. */
17
+ export interface ReaderArticle {
18
+ /** Whitelisted body markup, ready to render. */
19
+ readonly html: string;
20
+ /** True when the body is known to be incomplete (size cap or partial parse). */
21
+ readonly truncated: boolean;
22
+ /** Set when nothing usable could be extracted. */
23
+ readonly error?: string;
24
+ }
25
+ /**
26
+ * The capture package's Remote face (the ingest proposal's M1), mirrored
27
+ * structurally — never imported. Absent namespaces probe as undefined.
28
+ */
29
+ export interface ReaderCaptureRemote {
30
+ /** Render a URL in the managed browser and hand back the serialized page. */
31
+ render: (request: {
32
+ url: string;
33
+ timeoutMs?: number;
34
+ }) => Promise<RemoteResult<{
35
+ html: string;
36
+ finalUrl?: string;
37
+ title?: string;
38
+ truncated?: boolean;
39
+ }>>;
40
+ }
41
+ /**
42
+ * What the pane's own extraction read from the article, beyond the stored body:
43
+ * the article's own title and a short excerpt. A saved link's card upgrades
44
+ * from the URL-derived label to these; a feed entry's title is already the
45
+ * publisher's and is never overridden.
46
+ */
47
+ export interface ReaderExtractedMeta {
48
+ readonly title?: string;
49
+ readonly excerpt?: string;
50
+ }
51
+ /**
52
+ * The business face every reader surface receives from its registration.
53
+ *
54
+ * The wire verbs come straight off the Remote; the last four are local
55
+ * gestures the pane cannot perform itself (they need the session scope, the
56
+ * clipboard, or the browser), so `client/index.ts` injects them.
57
+ */
58
+ export interface ReaderPaneInjected {
59
+ /** Round-trip the capability handshake. */
60
+ capabilities: () => Promise<RemoteResult<ReaderCapabilities>>;
61
+ /** The configured sources, newest first. */
62
+ listSources: () => Promise<RemoteResult<{
63
+ sources: ReaderSourceSummary[];
64
+ }>>;
65
+ /** Add a feed or a pasted article; the host decides which by content. */
66
+ addSource: (url: string) => Promise<RemoteResult<ReaderAddOutcome | ReaderAddRefusal | ReaderAddFailure>>;
67
+ /** Change a source's label/enabled flag, or the global refresh time. */
68
+ updateSource: (request: {
69
+ id: string;
70
+ enabled?: boolean;
71
+ label?: string;
72
+ url?: string;
73
+ timeOfDay?: string;
74
+ }) => Promise<RemoteResult<ReaderMutationOutcome>>;
75
+ /** Drop a source. Its entries' translation maps go with it (the caller passes the ids it parsed). */
76
+ removeSource: (id: string, entryIds?: readonly string[]) => Promise<RemoteResult<ReaderMutationOutcome>>;
77
+ /** Fetch the named sources, or every enabled one. */
78
+ refresh: (ids?: string[]) => Promise<RemoteResult<{
79
+ results: ReaderRefreshResult[];
80
+ }>>;
81
+ /** Raw payloads, for this process to parse (the host never parses). */
82
+ getBodies: (ids: string[]) => Promise<RemoteResult<{
83
+ bodies: ReaderBody[];
84
+ }>>;
85
+ /** Offer a ref block to the side-chat service when one is composed. */
86
+ quoteToSideChat: (request: {
87
+ contextKey: string;
88
+ label: string;
89
+ text: string;
90
+ }) => Promise<RemoteResult<'ok' | 'unavailable'>>;
91
+ /** What the plugin holds per entry: a body, a stored payload, or a failure. */
92
+ entryFetchStates: (entryIds: readonly string[]) => Promise<RemoteResult<{
93
+ states: Record<string, ReaderEntryFetchState>;
94
+ }>>;
95
+ /** Cache body markup this process holds (the sweep's extractions, or a feed's own full text on open). */
96
+ storeEntryBody: (request: {
97
+ entryId: string;
98
+ url: string;
99
+ html: string;
100
+ truncated?: boolean;
101
+ scriptFigures?: number;
102
+ /** This body came from the capture rendered fetch; 「重新抓取」 re-renders rather than plain-fetches it. */
103
+ rendered?: boolean;
104
+ /** `translationHash(html)` — the entry's translation map dies with a body it no longer matches. */
105
+ bodyHash?: string;
106
+ /** The article's own title, as extracted — a saved link's card upgrade. */
107
+ title?: string;
108
+ /** A short excerpt (the first substantial paragraph), with the title. */
109
+ excerpt?: string;
110
+ }) => Promise<RemoteResult<ReaderEntryBodyView>>;
111
+ /** A payload the host stored, so this process can extract it (late or never). */
112
+ getRawBody: (entryId: string) => Promise<RemoteResult<{
113
+ entryId: string;
114
+ raw?: string;
115
+ url?: string;
116
+ truncated?: boolean;
117
+ error?: string;
118
+ }>>;
119
+ /**
120
+ * The entries whose full text still has to be filled in (the automatic
121
+ * backfill's work list; the host owns the policy, this process owns the DOM).
122
+ */
123
+ listBackfillCandidates: (entries: readonly {
124
+ entryId: string;
125
+ url: string;
126
+ label: string;
127
+ hasBody: boolean;
128
+ }[]) => Promise<RemoteResult<{
129
+ candidates: ReaderBackfillCandidate[];
130
+ }>>;
131
+ /**
132
+ * What the host has for one entry: a fresh cached body, else the feed's own
133
+ * payload, else nothing (plus why a previous fetch failed).
134
+ */
135
+ getEntryBody: (request: {
136
+ entryId: string;
137
+ url: string;
138
+ feedHtml?: string;
139
+ }) => Promise<RemoteResult<ReaderEntryBodyView>>;
140
+ /**
141
+ * Fetch one entry's article, extract it in THIS process (the host has no
142
+ * parser) and cache what came out. One call from the view's perspective; the
143
+ * extracted title/excerpt ride along for the card upgrade.
144
+ */
145
+ fetchEntryBody: (entryId: string, url: string) => Promise<ReaderEntryBodyView & ReaderExtractedMeta>;
146
+ /** The tags on one entry. */
147
+ entryTags: (entryId: string) => Promise<RemoteResult<{
148
+ tags: ReaderTag[];
149
+ }>>;
150
+ /** The tag vocabulary, with per-tag usage counts. */
151
+ listTags: () => Promise<RemoteResult<{
152
+ tags: ReaderTag[];
153
+ counts: Record<string, number>;
154
+ }>>;
155
+ /** Create a tag, or return the existing one whose name matches. */
156
+ createTag: (name: string) => Promise<RemoteResult<ReaderTag | ReaderAnnotationOutcome>>;
157
+ /** Add or remove one tag on one entry. */
158
+ tagEntry: (entryId: string, tagId: string, on: boolean) => Promise<RemoteResult<ReaderAnnotationOutcome>>;
159
+ /** Rename a tag. */
160
+ renameTag: (id: string, name: string) => Promise<RemoteResult<ReaderAnnotationOutcome>>;
161
+ /** Delete a tag everywhere. */
162
+ deleteTag: (id: string) => Promise<RemoteResult<ReaderAnnotationOutcome>>;
163
+ /** Drop tags nothing references any more. */
164
+ pruneTags: () => Promise<RemoteResult<{
165
+ removed: number;
166
+ }>>;
167
+ /** Record that the reader opened one entry (the 「最近阅读」 page's write half). */
168
+ recordRead: (request: {
169
+ entryId: string;
170
+ sourceId: string;
171
+ title: string;
172
+ url?: string;
173
+ }) => Promise<RemoteResult<{
174
+ entries: number;
175
+ }>>;
176
+ /** The entries the reader opened, newest first. */
177
+ listRecent: () => Promise<RemoteResult<{
178
+ entries: ReaderRecentEntry[];
179
+ }>>;
180
+ /** Forget every recent entry. */
181
+ clearRecent: () => Promise<RemoteResult<{
182
+ removed: number;
183
+ }>>;
184
+ /** Read / set the article cache policy (and the translation budget). */
185
+ getCachePolicy: () => Promise<RemoteResult<{
186
+ ttlHours: number;
187
+ maxEntries: number;
188
+ translationBudgetChars: number;
189
+ }>>;
190
+ setCachePolicy: (ttlHours: number, maxEntries?: number, translationBudgetChars?: number) => Promise<RemoteResult<ReaderAnnotationOutcome>>;
191
+ /** One entry's exact-fit translation record, when the host holds a usable one. */
192
+ getEntryTranslation: (entryId: string) => Promise<RemoteResult<{
193
+ translation?: ReaderEntryTranslationView;
194
+ }>>;
195
+ /** Translations for exactly the asked sentence hashes of one pair (never the table). */
196
+ getSentenceTranslations: (request: {
197
+ pair: string;
198
+ hashes: readonly string[];
199
+ }) => Promise<RemoteResult<{
200
+ translations: Record<string, string>;
201
+ }>>;
202
+ /**
203
+ * Persist what one translation run learned (and refresh what it reused), in
204
+ * one batch — the caller batches by run, so this is never a per-sentence call.
205
+ */
206
+ rememberSentences: (request: {
207
+ pair: string;
208
+ entries: readonly ReaderSentenceLearn[];
209
+ recalled?: readonly ReaderSentenceLearn[];
210
+ entryId?: string;
211
+ bodyHash?: string;
212
+ }) => Promise<RemoteResult<{
213
+ stored: number;
214
+ }>>;
215
+ /** Per-tier cache usage, aggregated on the host (the manage page's readout). */
216
+ getStorageStats: () => Promise<RemoteResult<ReaderStorageStats>>;
217
+ /** Forget every translation: the global memory and every entry map. */
218
+ clearTranslations: () => Promise<RemoteResult<{
219
+ clearedEntries: number;
220
+ clearedMemory: boolean;
221
+ }>>;
222
+ /** Read the current conversation draft (the merge `setDraft` needs). */
223
+ readDraft: () => string;
224
+ /** Replace the conversation draft with `merged`; a no-op without a session surface. */
225
+ setDraft: (merged: string) => void;
226
+ /** Copy text to the clipboard; resolves false when the API is unavailable. */
227
+ copyText: (text: string) => Promise<boolean>;
228
+ /** Open a URL in the user's browser; false when the platform refuses. */
229
+ openExternal: (url: string) => boolean;
230
+ /**
231
+ * Whether the host's in-app Sidebar Browser is mounted (probed live: the tab
232
+ * kind `browser` exists on host 0.1.6-alpha.2+). When it is, fetches that
233
+ * need a real browser — a page that draws its figures with scripts, a bot
234
+ * wall — get an in-app way out instead of only an external link.
235
+ */
236
+ browserTabAvailable: () => boolean;
237
+ /**
238
+ * Open a URL in the in-app Sidebar Browser; false when the seam is absent or
239
+ * refuses, so the caller falls back to {@link ReaderPaneInjected.openExternal}.
240
+ */
241
+ openBrowserTab: (url: string) => boolean;
242
+ /**
243
+ * The capture package's Remote, when one is mounted — the 「渲染抓取」 slot
244
+ * renders only while this answers. M0 ships no capture package (it is the
245
+ * ingest proposal's M1), so the probe is the whole point today: probed live,
246
+ * never cached, never thrown.
247
+ */
248
+ captureRemote: () => ReaderCaptureRemote | undefined;
249
+ }
250
+ /** Full props of the reader pane: the seat's session + store + locale + face. */
251
+ export type ReaderPaneProps = {
252
+ sessionId: SessionId;
253
+ } & GlobalStandardProps & PropsStore<ReturnType<typeof createReaderStore>> & InjectFace<ReaderPaneInjected> & PropsLocale<'reader'>;
254
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Stage one of the reader's right-Sidebar registration: the tab type, the body
3
+ * of which is registered in `client/index.ts`.
4
+ *
5
+ * The kind is MINTED (`reader`) rather than taken over: this is a page type of
6
+ * its own, not a superset of an official one, so it claims no address and ships
7
+ * no `patterns` / `canOpen`. It joins the guide page as its own card, which is
8
+ * how a reader finds it before pinning the tab.
9
+ *
10
+ * @module @khorsheed/dsh-reader/client/definition
11
+ */
12
+ import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client';
13
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
14
+ /** The tab kind: minted, since no official kind means "reader". */
15
+ export declare const READER_KIND = "reader";
16
+ /** This implementation's identity, and the key its body registers under. */
17
+ export declare const READER_TAB_ID = "@khorsheed/dsh-reader";
18
+ /**
19
+ * The reader type's registry definition. No `priority` (the default
20
+ * `extension` band is right for a type from outside the product) and no
21
+ * `patterns`/`canOpen` (a page type recognises no address).
22
+ *
23
+ * @param t - the reader namespace's translator.
24
+ * @returns the definition to register.
25
+ */
26
+ export declare function readerDefinition(t: TranslateNS<'reader'>): SidebarRightTabDefinition;
27
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { IconGlobeOutlineMedium } from "./icons.js";
3
+ /** The tab kind: minted, since no official kind means "reader". */
4
+ export const READER_KIND = 'reader';
5
+ /** This implementation's identity, and the key its body registers under. */
6
+ export const READER_TAB_ID = '@khorsheed/dsh-reader';
7
+ /** The type's glyph on the guide card. */
8
+ function ReaderGlyph({ size, className }) {
9
+ return _jsx(IconGlobeOutlineMedium, { size: size, className: className });
10
+ }
11
+ /**
12
+ * The reader type's registry definition. No `priority` (the default
13
+ * `extension` band is right for a type from outside the product) and no
14
+ * `patterns`/`canOpen` (a page type recognises no address).
15
+ *
16
+ * @param t - the reader namespace's translator.
17
+ * @returns the definition to register.
18
+ */
19
+ export function readerDefinition(t) {
20
+ return {
21
+ id: READER_TAB_ID,
22
+ kind: READER_KIND,
23
+ title: () => t('tab.label'),
24
+ guide: [{
25
+ id: READER_KIND,
26
+ order: 60,
27
+ title: () => t('tab.label'),
28
+ description: () => t('guide.description'),
29
+ icon: ReaderGlyph,
30
+ }],
31
+ };
32
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Article extraction and rich-text normalization for the browser half.
3
+ *
4
+ * Two exports, one for each shape of input the reader handles:
5
+ *
6
+ * - {@link extractArticle} takes a fetched HTML page and finds its body. The
7
+ * honest problem here is that which block is "the article" is a heuristic:
8
+ * this module scores candidate blocks the way readability implementations do
9
+ * (paragraph mass, minus link mass, with wrapper blocks demoted) and adds the
10
+ * one signal that a single-language scorer gets wrong — Chinese paragraphs
11
+ * are short by nature, so a length threshold tuned for English rejects them
12
+ * and the scorer falls back to the whole page container (measured: 473 <p>
13
+ * and 56 images instead of the article).
14
+ * - {@link normalizeRichText} takes markup that is ALREADY known to be a body
15
+ * (a feed's `content:encoded`, or the block extraction selected) and puts it
16
+ * through a tag whitelist, so the renderer never sees third-party scripts,
17
+ * styles, forms or event handlers.
18
+ *
19
+ * Everything runs in the browser because the host runtime has no DOM parser at
20
+ * all; nothing here touches the network.
21
+ *
22
+ * @module @khorsheed/dsh-reader/client/extract-article
23
+ */
24
+ /** Result of extracting one article page. */
25
+ export type ExtractArticleResult = {
26
+ readonly ok: true;
27
+ readonly html: string;
28
+ readonly textLength: number;
29
+ /**
30
+ * The article's own title, when one is recoverable: the body's first
31
+ * heading, else the document's `<title>`. A saved link's card upgrades from
32
+ * the URL-derived label to this — the wall is where "which paper was this"
33
+ * gets answered.
34
+ */
35
+ readonly title?: string;
36
+ /**
37
+ * A short plain-text excerpt (the first substantial paragraph, truncated at
38
+ * the card's measure), captured with the title.
39
+ */
40
+ readonly excerpt?: string;
41
+ /**
42
+ * How many figures had to be dropped because the page DRAWS them.
43
+ *
44
+ * Measured on the acceptance instance's transformer-circuits.pub paper:
45
+ * `<figure data-fignum="2">` contains an empty `<div class='intro-structural'>`
46
+ * and a caption, and the illustration is rendered by the page's own scripts.
47
+ * A fetch runs no scripts, so what arrives is a caption describing a picture
48
+ * that is not there. Dropping the empty figure and saying so beats leaving a
49
+ * bare caption the reader will read as "the plugin lost my image".
50
+ */
51
+ readonly scriptFigures?: number;
52
+ } | {
53
+ readonly ok: false;
54
+ readonly error: string;
55
+ };
56
+ /**
57
+ * Extract the article body from a fetched page.
58
+ *
59
+ * @param pageHtml - the raw HTML the host fetched.
60
+ * @param baseUrl - the page URL, used to absolutize relative links and images.
61
+ * @returns the normalized body HTML, or the reason nothing usable was found.
62
+ */
63
+ export declare function extractArticle(pageHtml: string, baseUrl: string): ExtractArticleResult;
64
+ /**
65
+ * Put trusted-shape markup through the whitelist.
66
+ *
67
+ * Feed bodies take this path: a feed's `content:encoded` is markup from the
68
+ * same publisher as the page, so it earns the same whitelist rather than the
69
+ * extraction heuristic.
70
+ *
71
+ * @param html - markup from a feed or from extraction.
72
+ * @param baseUrl - base for relative URLs (feed bodies usually use absolute ones).
73
+ * @returns whitelisted HTML, or an empty string when nothing survives.
74
+ */
75
+ export declare function normalizeRichText(html: string, baseUrl?: string): string;
76
+ /**
77
+ * The payload size that separates a real inlined figure from a 1px tracking
78
+ * placeholder. A transparent-gif placeholder is ~70 characters of base64; the
79
+ * smallest real chart is thousands — the threshold sits far from both, so no
80
+ * borderline page ever decides anything. (transformer-circuits.pub inlines its
81
+ * real figures as multi-hundred-KB data URIs; the old all-data:-is-placeholder
82
+ * rule deleted them.)
83
+ */
84
+ export declare const DATA_IMAGE_MIN_PAYLOAD = 512;
85
+ /**
86
+ * A `data:` URI worth keeping as an image, or `undefined` (which reads as
87
+ * ABSENT — the caller falls through to the lazy-loading attributes).
88
+ *
89
+ * The gate is MIME plus payload size; base64 payloads are whitespace-stripped
90
+ * because pages line-wrap them (a raw newline inside an emitted attribute is
91
+ * legal HTML, but the stripped form is what browsers parse either way).
92
+ */
93
+ export declare function substantiveDataImage(value: string): string | undefined;
94
+ /**
95
+ * Turn an inline-only rendering of a node into plain text-safe markup. Feed
96
+ * summaries use this shape when the caller wants emphasis but no blocks.
97
+ *
98
+ * @param html - markup.
99
+ * @returns normalized inline markup.
100
+ */
101
+ export declare function normalizeInline(html: string): string;
102
+ //# sourceMappingURL=extract-article.d.ts.map