@songmu/mdhq 0.0.2
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 +21 -0
- package/README.md +126 -0
- package/dist/assets/localize.d.ts +19 -0
- package/dist/assets/localize.js +364 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +119 -0
- package/dist/config/config.d.ts +25 -0
- package/dist/config/config.js +170 -0
- package/dist/config/match.d.ts +7 -0
- package/dist/config/match.js +101 -0
- package/dist/convert/article-date.d.ts +20 -0
- package/dist/convert/article-date.js +255 -0
- package/dist/convert/convert-html.d.ts +2 -0
- package/dist/convert/convert-html.js +89 -0
- package/dist/convert/extract-published.d.ts +12 -0
- package/dist/convert/extract-published.js +24 -0
- package/dist/convert/extract-updated.d.ts +8 -0
- package/dist/convert/extract-updated.js +20 -0
- package/dist/date.d.ts +18 -0
- package/dist/date.js +448 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +10 -0
- package/dist/frontmatter/frontmatter.d.ts +40 -0
- package/dist/frontmatter/frontmatter.js +114 -0
- package/dist/get-page.d.ts +2 -0
- package/dist/get-page.js +308 -0
- package/dist/http/fetch.d.ts +46 -0
- package/dist/http/fetch.js +195 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/list-files.d.ts +8 -0
- package/dist/list-files.js +35 -0
- package/dist/markdown/transform.d.ts +6 -0
- package/dist/markdown/transform.js +129 -0
- package/dist/path/storage-path.d.ts +7 -0
- package/dist/path/storage-path.js +110 -0
- package/dist/storage/atomic.d.ts +8 -0
- package/dist/storage/atomic.js +84 -0
- package/dist/storage/path-safety.d.ts +1 -0
- package/dist/storage/path-safety.js +55 -0
- package/dist/storage/save.d.ts +23 -0
- package/dist/storage/save.js +118 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/dist/url/identity.d.ts +12 -0
- package/dist/url/identity.js +54 -0
- package/dist/url/pathname.d.ts +4 -0
- package/dist/url/pathname.js +46 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +6 -0
- package/docs/README.md +14 -0
- package/docs/configuration.md +242 -0
- package/docs/library-api.md +275 -0
- package/docs/specification.md +730 -0
- package/package.json +73 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MdhqError } from "../errors.js";
|
|
2
|
+
export const HTML_EXTENSIONS = new Set([
|
|
3
|
+
".html",
|
|
4
|
+
".htm",
|
|
5
|
+
".xhtml",
|
|
6
|
+
".php",
|
|
7
|
+
".asp",
|
|
8
|
+
".aspx",
|
|
9
|
+
".jsp",
|
|
10
|
+
".jspx"
|
|
11
|
+
]);
|
|
12
|
+
export function decodeUrlPathSegment(segment) {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(segment.replace(/%(?![0-9a-f]{2})/giu, "%25")).normalize("NFC");
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new MdhqError("INVALID_URL", `Invalid UTF-8 in path segment: ${segment}`, {
|
|
18
|
+
cause: error
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function storageBasename(segment) {
|
|
23
|
+
const normalized = decodeUrlPathSegment(segment);
|
|
24
|
+
const match = normalized.match(/(\.[^.]+)$/u);
|
|
25
|
+
const extension = match?.[1];
|
|
26
|
+
return extension && HTML_EXTENSIONS.has(extension.toLowerCase())
|
|
27
|
+
? normalized.slice(0, -extension.length)
|
|
28
|
+
: normalized;
|
|
29
|
+
}
|
|
30
|
+
export function canonicalPathname(pathname, hasEntryValue = false) {
|
|
31
|
+
const segments = pathname
|
|
32
|
+
.split("/")
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((segment) => encodeURIComponent(decodeUrlPathSegment(segment)));
|
|
35
|
+
if (!hasEntryValue) {
|
|
36
|
+
if (segments.length === 0) {
|
|
37
|
+
return "/index";
|
|
38
|
+
}
|
|
39
|
+
const finalIndex = segments.length - 1;
|
|
40
|
+
const finalSegment = pathname.split("/").filter(Boolean).at(-1);
|
|
41
|
+
if (finalSegment !== undefined) {
|
|
42
|
+
segments[finalIndex] = encodeURIComponent(storageBasename(finalSegment));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return `/${segments.join("/")}`;
|
|
46
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const packageJson = require("../package.json");
|
|
4
|
+
export const VERSION = packageJson.version;
|
|
5
|
+
export const PROJECT_URL = "https://github.com/Songmu/mdhq";
|
|
6
|
+
export const DEFAULT_USER_AGENT = `mdhq/${VERSION} (+${PROJECT_URL})`;
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# mdhq documentation
|
|
2
|
+
|
|
3
|
+
This directory documents the behavior implemented in mdhq `0.0.0`.
|
|
4
|
+
|
|
5
|
+
- [Current specification](specification.md) describes CLI behavior, URL
|
|
6
|
+
identity, HTTP fetching, storage paths, Markdown processing, assets,
|
|
7
|
+
frontmatter, and concurrent writes.
|
|
8
|
+
- [Configuration reference](configuration.md) describes the JSON
|
|
9
|
+
configuration file, XDG paths, option precedence, and host/path matching.
|
|
10
|
+
- [Library API reference](library-api.md) describes the public exports,
|
|
11
|
+
options, results, warnings, and errors.
|
|
12
|
+
|
|
13
|
+
These documents describe the current implementation. They are not a roadmap
|
|
14
|
+
for unimplemented features.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
mdhq reads one optional JSON configuration file.
|
|
4
|
+
|
|
5
|
+
Default location:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
$XDG_CONFIG_HOME/mdhq/config.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
When `XDG_CONFIG_HOME` is unset:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
~/.config/mdhq/config.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The library can select another file with `GetPageOptions.configPath`.
|
|
18
|
+
The CLI does not currently expose a configuration-path option.
|
|
19
|
+
|
|
20
|
+
A missing configuration file is accepted. An unreadable file, malformed
|
|
21
|
+
JSON, a non-object top-level value, or an invalid known value is a fatal
|
|
22
|
+
`CONFIG_ERROR`.
|
|
23
|
+
|
|
24
|
+
Unknown keys are accepted and reported with `UNKNOWN_CONFIG_KEY` warnings.
|
|
25
|
+
This applies at the top level and within `defuddle`, `frontmatter`, host
|
|
26
|
+
configuration, and path configuration objects.
|
|
27
|
+
|
|
28
|
+
## Complete example
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"root": "/data/mdhq",
|
|
33
|
+
"userAgent": "my-clipper/1.0",
|
|
34
|
+
"timeoutMs": 30000,
|
|
35
|
+
"maxResponseBytes": 20971520,
|
|
36
|
+
"maxRedirects": 10,
|
|
37
|
+
"assets": false,
|
|
38
|
+
"useAsync": true,
|
|
39
|
+
"defuddle": {
|
|
40
|
+
"removeSmallImages": true,
|
|
41
|
+
"standardize": true,
|
|
42
|
+
"language": "en"
|
|
43
|
+
},
|
|
44
|
+
"frontmatter": {
|
|
45
|
+
"exclude": ["description"],
|
|
46
|
+
"values": {
|
|
47
|
+
"collection": "reading",
|
|
48
|
+
"reviewed": false
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"hosts": {
|
|
52
|
+
"*.example.com": {
|
|
53
|
+
"entryQueryKey": "entry_id",
|
|
54
|
+
"paths": {
|
|
55
|
+
"/articles/*.php": {
|
|
56
|
+
"entryQueryKey": "id"
|
|
57
|
+
},
|
|
58
|
+
"/search/*": {
|
|
59
|
+
"entryQueryKey": null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Top-level fields
|
|
68
|
+
|
|
69
|
+
| Field | Type | Description |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `root` | string | Default storage root. |
|
|
72
|
+
| `userAgent` | string | Default page and asset User-Agent. |
|
|
73
|
+
| `timeoutMs` | positive integer | Timeout in milliseconds for each mdhq HTTP request attempt. |
|
|
74
|
+
| `maxResponseBytes` | positive integer | Maximum buffered bytes for each page or asset response. |
|
|
75
|
+
| `maxRedirects` | non-negative integer | Maximum redirect count for each page or asset request. |
|
|
76
|
+
| `assets` | boolean | Download images into `_assets`; defaults to `true`. |
|
|
77
|
+
| `useAsync` | boolean | Legacy fallback for Defuddle asynchronous extractors. |
|
|
78
|
+
| `defuddle` | object | Defuddle extraction options. |
|
|
79
|
+
| `frontmatter` | object | Frontmatter exclusions and configured values. |
|
|
80
|
+
| `hosts` | object | Host and pathname-specific entry query configuration. |
|
|
81
|
+
|
|
82
|
+
## Storage root precedence
|
|
83
|
+
|
|
84
|
+
The effective root is selected in this order:
|
|
85
|
+
|
|
86
|
+
1. CLI `--root` or library `GetPageOptions.root`
|
|
87
|
+
2. `MDHQ_ROOT`
|
|
88
|
+
3. configuration `root`
|
|
89
|
+
4. `$XDG_DATA_HOME/mdhq`
|
|
90
|
+
5. `~/.local/share/mdhq` when `XDG_DATA_HOME` is unset
|
|
91
|
+
|
|
92
|
+
The selected root is converted to an absolute path.
|
|
93
|
+
|
|
94
|
+
## HTTP option precedence
|
|
95
|
+
|
|
96
|
+
For each setting, a defined `GetPageOptions` value overrides configuration.
|
|
97
|
+
Otherwise configuration overrides the built-in default.
|
|
98
|
+
|
|
99
|
+
| Library option | Configuration field | Built-in default |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `userAgent` | `userAgent` | mdhq version User-Agent |
|
|
102
|
+
| `timeoutMs` | `timeoutMs` | `30000` |
|
|
103
|
+
| `maxResponseBytes` | `maxResponseBytes` | `20971520` |
|
|
104
|
+
| `maxRedirects` | `maxRedirects` | `10` |
|
|
105
|
+
|
|
106
|
+
The CLI `--user-agent` option is passed as `GetPageOptions.userAgent` and
|
|
107
|
+
therefore overrides configuration `userAgent`.
|
|
108
|
+
|
|
109
|
+
The CLI `--no-assets` option passes `GetPageOptions.assets: false` and
|
|
110
|
+
therefore overrides configuration `assets`. When asset localization is
|
|
111
|
+
disabled, image destinations remain absolute URLs, the result contains no
|
|
112
|
+
asset entries, and mdhq does not create `_assets`.
|
|
113
|
+
|
|
114
|
+
Generic CLI `--header` values and library `headers` values are appended after
|
|
115
|
+
mdhq creates its `Accept` and User-Agent headers. A generic `User-Agent` or
|
|
116
|
+
`Accept` entry is combined with the existing value; it does not replace it.
|
|
117
|
+
Use the dedicated User-Agent option for replacement.
|
|
118
|
+
|
|
119
|
+
`GetPageOptions.headers` has no configuration equivalent. This prevents
|
|
120
|
+
credentials such as cookies or authorization tokens from being stored in the
|
|
121
|
+
configuration file.
|
|
122
|
+
|
|
123
|
+
## Defuddle options
|
|
124
|
+
|
|
125
|
+
Supported `defuddle` fields:
|
|
126
|
+
|
|
127
|
+
| Field | Type |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `debug` | boolean |
|
|
130
|
+
| `removeExactSelectors` | boolean |
|
|
131
|
+
| `removePartialSelectors` | boolean |
|
|
132
|
+
| `removeImages` | boolean |
|
|
133
|
+
| `useAsync` | boolean |
|
|
134
|
+
| `removeHiddenElements` | boolean |
|
|
135
|
+
| `removeLowScoring` | boolean |
|
|
136
|
+
| `removeSmallImages` | boolean |
|
|
137
|
+
| `standardize` | boolean |
|
|
138
|
+
| `removeContentPatterns` | boolean |
|
|
139
|
+
| `contentSelector` | string |
|
|
140
|
+
| `language` | string |
|
|
141
|
+
| `includeReplies` | boolean or `"extractors"` |
|
|
142
|
+
| `profile` | boolean |
|
|
143
|
+
|
|
144
|
+
mdhq always enables Defuddle Markdown output and supplies its own
|
|
145
|
+
proxy-aware fetch implementation. Those values are not configurable through
|
|
146
|
+
the JSON file.
|
|
147
|
+
|
|
148
|
+
The supplied fetch implementation adds environment proxy handling only.
|
|
149
|
+
Defuddle-internal asynchronous requests do not inherit mdhq generic
|
|
150
|
+
headers, the configured mdhq User-Agent, or mdhq's timeout,
|
|
151
|
+
response-size, and redirect limits.
|
|
152
|
+
|
|
153
|
+
The effective asynchronous-extractor setting is:
|
|
154
|
+
|
|
155
|
+
1. `GetPageOptions.useAsync`
|
|
156
|
+
2. `defuddle.useAsync`
|
|
157
|
+
3. top-level `useAsync`
|
|
158
|
+
4. `true`
|
|
159
|
+
|
|
160
|
+
## Frontmatter configuration
|
|
161
|
+
|
|
162
|
+
`frontmatter.exclude` is an array of field names to remove from extracted or
|
|
163
|
+
derived metadata.
|
|
164
|
+
|
|
165
|
+
`frontmatter.values` is an object of scalar fixed values. Accepted values are
|
|
166
|
+
strings, numbers, booleans, and `null`.
|
|
167
|
+
|
|
168
|
+
Application order:
|
|
169
|
+
|
|
170
|
+
1. Add extracted metadata and representative-image fields.
|
|
171
|
+
2. Remove fields listed in `exclude`.
|
|
172
|
+
3. Apply fields from `values`.
|
|
173
|
+
4. Add protected mdhq fields.
|
|
174
|
+
|
|
175
|
+
Protected fields are:
|
|
176
|
+
|
|
177
|
+
- `source`
|
|
178
|
+
- `requested_url`
|
|
179
|
+
- `created`
|
|
180
|
+
- `modified`
|
|
181
|
+
- `content_digest`
|
|
182
|
+
- `etag`
|
|
183
|
+
- `last_modified`
|
|
184
|
+
- `vary`
|
|
185
|
+
|
|
186
|
+
`content_digest` and `vary` are reserved legacy names and are removed from
|
|
187
|
+
serialized frontmatter even if configured in `frontmatter.values`.
|
|
188
|
+
|
|
189
|
+
mdhq does not add a `type` property by default. Add one explicitly when
|
|
190
|
+
desired:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"frontmatter": {
|
|
195
|
+
"values": {
|
|
196
|
+
"type": "clip"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Protected fields cannot be removed or overridden by configuration.
|
|
203
|
+
|
|
204
|
+
## Host and path matching
|
|
205
|
+
|
|
206
|
+
`hosts` is an object whose keys are exact host strings or minimatch glob
|
|
207
|
+
patterns.
|
|
208
|
+
|
|
209
|
+
Before matching, URL hosts are lowercased, converted to IDNA ASCII, and
|
|
210
|
+
normalized for trailing DNS dots and ports. Host patterns are also lowercased
|
|
211
|
+
and literal labels are converted to IDNA ASCII. Explicit ports in patterns
|
|
212
|
+
are retained. A pattern such as `example.com:443` therefore matches an
|
|
213
|
+
explicit non-standard HTTP port 443, while an HTTPS URL using its standard
|
|
214
|
+
port matches `example.com`.
|
|
215
|
+
|
|
216
|
+
Selection order:
|
|
217
|
+
|
|
218
|
+
1. A normalized exact match.
|
|
219
|
+
2. The matching glob with the greatest number of fixed literal characters.
|
|
220
|
+
3. No host configuration.
|
|
221
|
+
|
|
222
|
+
If multiple matching patterns have equal specificity, processing fails with
|
|
223
|
+
`CONFIG_ERROR`. Multiple patterns that normalize to the same exact host are
|
|
224
|
+
also an error.
|
|
225
|
+
|
|
226
|
+
After selecting a host, `paths` is matched against the URL's encoded
|
|
227
|
+
`pathname` using the same exact-then-most-specific policy. Path patterns are
|
|
228
|
+
not IDNA- or Unicode-normalized.
|
|
229
|
+
|
|
230
|
+
## Entry query key inheritance
|
|
231
|
+
|
|
232
|
+
`entryQueryKey` selects the only query parameter that contributes to URL
|
|
233
|
+
identity and the storage path.
|
|
234
|
+
|
|
235
|
+
Rules:
|
|
236
|
+
|
|
237
|
+
- A host-level value applies to all paths by default.
|
|
238
|
+
- A matching path-level string overrides the host value.
|
|
239
|
+
- A matching path-level `null` disables the host value.
|
|
240
|
+
- A matching path object that omits `entryQueryKey` inherits the host value.
|
|
241
|
+
- A missing or empty parameter value falls back to normal path-based storage.
|
|
242
|
+
- All non-selected query parameters are ignored.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Library API reference
|
|
2
|
+
|
|
3
|
+
The package is an ECMAScript module:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
convertHtml,
|
|
8
|
+
getPage,
|
|
9
|
+
MdhqError
|
|
10
|
+
} from "@songmu/mdhq";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Runtime exports:
|
|
14
|
+
|
|
15
|
+
- `convertHtml`
|
|
16
|
+
- `getPage`
|
|
17
|
+
- `MdhqError`
|
|
18
|
+
|
|
19
|
+
Type-only exports:
|
|
20
|
+
|
|
21
|
+
- `MdhqConfig`
|
|
22
|
+
- `MdhqErrorCode`
|
|
23
|
+
- `AssetResult`
|
|
24
|
+
- `ConvertedPage`
|
|
25
|
+
- `ConvertHtmlOptions`
|
|
26
|
+
- `GetPageOptions`
|
|
27
|
+
- `GetPageResult`
|
|
28
|
+
- `HeaderValue`
|
|
29
|
+
- `MdhqWarning`
|
|
30
|
+
- `PageMetadata`
|
|
31
|
+
|
|
32
|
+
TypeScript callers should import types with `import type`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { getPage, MdhqError } from "@songmu/mdhq";
|
|
36
|
+
import type { GetPageOptions, GetPageResult, MdhqConfig } from "@songmu/mdhq";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `convertHtml`
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
function convertHtml(options: ConvertHtmlOptions): Promise<ConvertedPage>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This is the low-level conversion API. It does not fetch the page, download
|
|
46
|
+
assets, add frontmatter, or write files.
|
|
47
|
+
|
|
48
|
+
### Options
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface ConvertHtmlOptions {
|
|
52
|
+
html: string;
|
|
53
|
+
url: string | URL;
|
|
54
|
+
defuddle?: Omit<DefuddleOptions, "markdown" | "url">;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `html` is the complete input HTML string.
|
|
59
|
+
- `url` is an absolute base URL. Unlike `getPage`, it is not restricted to
|
|
60
|
+
HTTP or HTTPS; for example, a `file:` URL is accepted.
|
|
61
|
+
- `defuddle` passes options to Defuddle.
|
|
62
|
+
|
|
63
|
+
mdhq forces `markdown: true`. `useAsync` defaults to `true` when it is not
|
|
64
|
+
provided.
|
|
65
|
+
|
|
66
|
+
### Result
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface ConvertedPage {
|
|
70
|
+
markdown: string;
|
|
71
|
+
metadata: PageMetadata;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface PageMetadata {
|
|
77
|
+
title?: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
author?: string;
|
|
80
|
+
published?: string;
|
|
81
|
+
updated?: string;
|
|
82
|
+
site?: string;
|
|
83
|
+
domain?: string;
|
|
84
|
+
language?: string;
|
|
85
|
+
image?: string;
|
|
86
|
+
favicon?: string;
|
|
87
|
+
wordCount?: number;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Empty string metadata is omitted. `wordCount` is omitted unless it is greater
|
|
92
|
+
than zero.
|
|
93
|
+
|
|
94
|
+
## `getPage`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
function getPage(options: GetPageOptions): Promise<GetPageResult>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This is the high-level fetch, convert, localize, and save API.
|
|
101
|
+
|
|
102
|
+
### Options
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
interface GetPageOptions {
|
|
106
|
+
url: string | URL;
|
|
107
|
+
root?: string;
|
|
108
|
+
configPath?: string;
|
|
109
|
+
assets?: boolean;
|
|
110
|
+
update?: boolean;
|
|
111
|
+
headers?: HeaderValue[];
|
|
112
|
+
userAgent?: string;
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
maxResponseBytes?: number;
|
|
115
|
+
maxRedirects?: number;
|
|
116
|
+
useAsync?: boolean;
|
|
117
|
+
now?: () => Date;
|
|
118
|
+
onWarning?: (warning: MdhqWarning) => void;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Field | Behavior |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `url` | Required HTTP or HTTPS page URL. |
|
|
125
|
+
| `root` | Highest-precedence library storage root. |
|
|
126
|
+
| `configPath` | Override the default XDG configuration path. |
|
|
127
|
+
| `assets` | Download and localize images when `true`; overrides configuration and defaults to `true`. When `false`, image destinations remain absolute and `_assets` is not created. |
|
|
128
|
+
| `update` | Replace an existing same-identity document when `true`; defaults to `false`. |
|
|
129
|
+
| `headers` | Additional page headers. Sent only while requests remain on the initial page origin. A cross-origin page redirect permanently disables them for later page and asset requests in that operation. |
|
|
130
|
+
| `userAgent` | Override the configured or built-in User-Agent. |
|
|
131
|
+
| `timeoutMs` | Override the configured or built-in per-request timeout. |
|
|
132
|
+
| `maxResponseBytes` | Override the configured or built-in response-size limit. |
|
|
133
|
+
| `maxRedirects` | Override the configured or built-in redirect limit. |
|
|
134
|
+
| `useAsync` | Highest-precedence Defuddle asynchronous-extractor setting. |
|
|
135
|
+
| `now` | Clock injection used for `created` and `modified`; intended for deterministic callers and tests. Both fields are written on initial acquisition. |
|
|
136
|
+
| `onWarning` | Called once for each warning as it is produced. |
|
|
137
|
+
|
|
138
|
+
### Headers
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
interface HeaderValue {
|
|
142
|
+
name: string;
|
|
143
|
+
value: string;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Multiple entries with the same name are appended.
|
|
148
|
+
|
|
149
|
+
### Result
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
interface GetPageResult {
|
|
153
|
+
requestedUrl: string;
|
|
154
|
+
sourceUrl: string;
|
|
155
|
+
path: string;
|
|
156
|
+
status: "saved" | "updated" | "unchanged" | "skipped";
|
|
157
|
+
assets: AssetResult[];
|
|
158
|
+
warnings: MdhqWarning[];
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `requestedUrl` is the input after WHATWG URL serialization. This lowercases
|
|
163
|
+
and IDNA-normalizes the host, removes an explicit default port, and retains
|
|
164
|
+
the query and fragment. This serialization is separate from URL identity,
|
|
165
|
+
which ignores the scheme, fragment, and unselected query parameters.
|
|
166
|
+
- `sourceUrl` is the WHATWG-serialized final URL maintained by the redirect
|
|
167
|
+
loop. Its query and fragment are whatever remain on that final URL after
|
|
168
|
+
URL resolution. It is exactly the value written to frontmatter `source`.
|
|
169
|
+
For a pre-fetch skip, it is the existing document's stored `source` value.
|
|
170
|
+
- `path` is the absolute Markdown path.
|
|
171
|
+
- `assets` is empty when page processing is skipped.
|
|
172
|
+
- `warnings` contains configuration and asset warnings.
|
|
173
|
+
|
|
174
|
+
Status meanings:
|
|
175
|
+
|
|
176
|
+
- `saved`: a new document was created.
|
|
177
|
+
- `updated`: an existing document's normalized Markdown body or user-facing
|
|
178
|
+
frontmatter changed.
|
|
179
|
+
- `unchanged`: HTTP returned 304, or a 200 response produced the same
|
|
180
|
+
normalized Markdown body and user-facing frontmatter. HTTP validators may
|
|
181
|
+
still be updated.
|
|
182
|
+
- `skipped`: an existing same-identity document was found without `update`.
|
|
183
|
+
|
|
184
|
+
With `update`, `getPage` sends the stored `etag` as `If-None-Match`, or falls
|
|
185
|
+
back to the stored `last_modified` as `If-Modified-Since`, when the request is
|
|
186
|
+
for the same HTTP target and does not include credentials. `etag` and
|
|
187
|
+
`last_modified` are stored in Markdown frontmatter only when the response is
|
|
188
|
+
safe to revalidate; `Vary` and body digests are not serialized.
|
|
189
|
+
|
|
190
|
+
### Asset results
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
interface AssetResult {
|
|
194
|
+
sourceUrl: string;
|
|
195
|
+
finalUrl?: string;
|
|
196
|
+
path?: string;
|
|
197
|
+
status: "saved" | "reused" | "failed";
|
|
198
|
+
error?: string;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- `sourceUrl` is the absolute pre-fetch asset URL discovered in Markdown or
|
|
203
|
+
representative-image metadata.
|
|
204
|
+
- `path` is the absolute local asset path.
|
|
205
|
+
- `saved` means a new immutable content-addressed asset file was created.
|
|
206
|
+
- `reused` means the deterministic asset path already existed with identical
|
|
207
|
+
bytes.
|
|
208
|
+
- `failed` means the Markdown operation continued without localizing that
|
|
209
|
+
asset, including the unlikely case of differing bytes at the same digest
|
|
210
|
+
and extension path.
|
|
211
|
+
- `finalUrl` and `path` are present for successful asset operations.
|
|
212
|
+
- `error` is present for failed operations.
|
|
213
|
+
|
|
214
|
+
The `assets` array follows first-discovery order. Markdown images are listed
|
|
215
|
+
in document traversal order, followed by the representative image when it was
|
|
216
|
+
not already discovered. Exact duplicate source URLs produce one result.
|
|
217
|
+
Different source URLs that redirect to the same final URL produce separate
|
|
218
|
+
results but reuse the same deterministic file.
|
|
219
|
+
|
|
220
|
+
### Warnings
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
interface MdhqWarning {
|
|
224
|
+
code: string;
|
|
225
|
+
message: string;
|
|
226
|
+
url?: string;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Current warning codes:
|
|
231
|
+
|
|
232
|
+
| Code | Meaning |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `UNKNOWN_CONFIG_KEY` | A JSON configuration key is not recognized but processing continues. |
|
|
235
|
+
| `ASSET_FETCH_FAILED` | An individual asset could not be fetched, validated, or saved. |
|
|
236
|
+
| `INVALID_IMAGE_URL` | Defuddle returned an invalid or non-HTTP(S) representative-image URL; the page is saved without that metadata field. |
|
|
237
|
+
| `INVALID_LAST_MODIFIED` | A response contained an invalid `Last-Modified` value; the page is saved without that validator. |
|
|
238
|
+
|
|
239
|
+
Warnings are both accumulated in the result and delivered to `onWarning`.
|
|
240
|
+
|
|
241
|
+
## Error model
|
|
242
|
+
|
|
243
|
+
Fatal operational errors are represented by:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
class MdhqError extends Error {
|
|
247
|
+
readonly code: MdhqErrorCode;
|
|
248
|
+
readonly cause?: unknown;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Current error codes:
|
|
253
|
+
|
|
254
|
+
| Code | Meaning |
|
|
255
|
+
| --- | --- |
|
|
256
|
+
| `INVALID_URL` | A URL or URL path encoding is invalid. |
|
|
257
|
+
| `INVALID_HEADER` | A CLI `--header` value does not use `Name: value` syntax. |
|
|
258
|
+
| `UNSUPPORTED_SCHEME` | The high-level operation received a non-HTTP(S) URL or a redirect used one. |
|
|
259
|
+
| `UNSUPPORTED_CONTENT_TYPE` | A page response was not HTML or XHTML. |
|
|
260
|
+
| `RESPONSE_TOO_LARGE` | A page exceeded the configured byte limit. The same condition for an individual asset is converted to an `ASSET_FETCH_FAILED` warning and failed asset result by `getPage`. |
|
|
261
|
+
| `TOO_MANY_REDIRECTS` | The configured redirect limit was exceeded. |
|
|
262
|
+
| `FETCH_FAILED` | Network, timeout, HTTP-status, or redirect metadata failure. |
|
|
263
|
+
| `CONVERSION_FAILED` | Defuddle failed or produced no usable Markdown. |
|
|
264
|
+
| `CONFIG_ERROR` | Configuration reading, syntax, type validation, or pattern selection failed. |
|
|
265
|
+
| `PATH_COLLISION` | A destination belongs to a different URL identity or cannot be identified safely. |
|
|
266
|
+
| `PATH_TOO_LONG` | The destination cannot fit after path-segment hashing. |
|
|
267
|
+
| `STORAGE_ERROR` | A Markdown read, create, temporary write, or replacement failed. |
|
|
268
|
+
|
|
269
|
+
Callers should branch on `error.code` rather than parsing `error.message`.
|
|
270
|
+
|
|
271
|
+
Errors produced while processing an individual asset do not escape
|
|
272
|
+
`getPage`. They are converted into `ASSET_FETCH_FAILED` warnings and
|
|
273
|
+
`AssetResult` objects with `status: "failed"`. The error table describes
|
|
274
|
+
fatal page-level and Markdown-storage failures unless a row explicitly says
|
|
275
|
+
otherwise.
|