@heroiclands/package-build 22.12.0 → 22.12.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/CHANGELOG.md +16 -0
- package/bin/content-build.mjs +4 -0
- package/docs/api.md +4 -2
- package/docs/content-format.md +33 -10
- package/docs/getting-started.md +12 -0
- package/engine/anchored-sections.mjs +2 -2
- package/engine/base-compiler.mjs +13 -0
- package/engine/bundles.mjs +2 -2
- package/engine/content-images.mjs +41 -14
- package/engine/content-secrets.mjs +110 -0
- package/engine/helpers.mjs +8 -1
- package/engine/journals.mjs +14 -3
- package/engine/pdf-build.mjs +13 -2
- package/engine/site-build.mjs +28 -4
- package/package.json +1 -1
- package/types/engine/content-images.d.mts +6 -3
- package/types/engine/content-secrets.d.mts +17 -0
- package/types/engine/helpers.d.mts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.12.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Content**
|
|
8
|
+
|
|
9
|
+
- GM passages in notes appear as secret sections in Foundry journals, expandable spoilers on the web, and labelled notes in books.
|
|
10
|
+
|
|
11
|
+
## 22.12.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
**Content authoring**
|
|
16
|
+
|
|
17
|
+
- Image embeds and Markdown images accept named size hints, so notes can carry the intended size while their displayed appearance stays the same.
|
|
18
|
+
|
|
3
19
|
## 22.12.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/bin/content-build.mjs
CHANGED
|
@@ -1627,6 +1627,9 @@ function siteCommand() {
|
|
|
1627
1627
|
message: e.reason,
|
|
1628
1628
|
});
|
|
1629
1629
|
}
|
|
1630
|
+
for (const e of result.secretErrors) {
|
|
1631
|
+
emitDiagnostic({ ...e, severity: "error" });
|
|
1632
|
+
}
|
|
1630
1633
|
// Reported the way the pack build reports the very same
|
|
1631
1634
|
// finding: `file:line:column: error: message`, path first, and
|
|
1632
1635
|
// the message from the shared table, not a
|
|
@@ -1663,6 +1666,7 @@ function siteCommand() {
|
|
|
1663
1666
|
}
|
|
1664
1667
|
if (
|
|
1665
1668
|
result.tableErrors.length ||
|
|
1669
|
+
result.secretErrors.length ||
|
|
1666
1670
|
result.wikiErrors.length ||
|
|
1667
1671
|
result.imageErrors.length
|
|
1668
1672
|
) {
|
package/docs/api.md
CHANGED
|
@@ -935,16 +935,17 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
|
|
|
935
935
|
|
|
936
936
|
### `engine.contentImages`
|
|
937
937
|
|
|
938
|
-
An image saying how wide it is and where it sits. A markdown image carries no indication of either, so each of the three surfaces decides for itself and the author — who is the one who knows — has no way to say. A directive in the curly-attribute convention Pandoc and Kramdown use closes that, in
|
|
938
|
+
An image saying how wide it is and where it sits. A markdown image carries no indication of either, so each of the three surfaces decides for itself and the author — who is the one who knows — has no way to say. A directive in the curly-attribute convention Pandoc and Kramdown use closes that, in three closed vocabularies: a width class, a named `size:`, and a `float:` position. The named size is accepted without affecting display.
|
|
939
939
|
|
|
940
940
|
| Export | Signature | Returns | Use it when |
|
|
941
941
|
| --------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
942
942
|
| `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
|
|
943
|
+
| `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts without applying a display size. |
|
|
943
944
|
| `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float:` positions an image may take, and where each puts it. |
|
|
944
945
|
| `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
|
|
945
946
|
| `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
|
|
946
947
|
| `imageSourceProblem` | `function imageSourceProblem(src)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an image's address, or `""` when nothing is. |
|
|
947
|
-
| `parseImageDirective` | `function parseImageDirective(raw)` | {{classes: string[], float: string, problems: string[]}} What was written, and what cannot be honoured.
|
|
948
|
+
| `parseImageDirective` | `function parseImageDirective(raw)` | {{classes: string[], size: string, float: string, problems: string[]}} What was written, and what cannot be honoured. | Read the directive on an image. |
|
|
948
949
|
| `figureClasses` | `function figureClasses(directive)` | {string} A space-separated class list. | The classes a figure carries, from a parsed directive. |
|
|
949
950
|
| `escapeHtml` | `function escapeHtml(text)` | {string} The same value, safe in markup. | Text going inside an HTML attribute or between tags. |
|
|
950
951
|
| `imageFigureHtml` | `function imageFigureHtml(image)` | {string} The figure, as one HTML block. | One image as the `<figure>` both HTML surfaces render. |
|
|
@@ -1011,6 +1012,7 @@ Shared helpers for the pack compilers in `packages/content-build/`. The HeroicLa
|
|
|
1011
1012
|
| Export | Signature | Returns | Use it when |
|
|
1012
1013
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1013
1014
|
| `md` | `const md` | — | The markdown renderer every surface shares. |
|
|
1015
|
+
| `renderFoundryMarkdown` | `function renderFoundryMarkdown(body)` | `string` — HTML with secret sections for GM passages. | Rendering a note body for a Foundry document. |
|
|
1014
1016
|
| `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter. |
|
|
1015
1017
|
| `assertStatedScope` | `function assertStatedScope(skipDirectories, who)` | {void} | Refuse a corpus read whose scope its caller did not state. |
|
|
1016
1018
|
| `assertSuppliedCorpus` | `function assertSuppliedCorpus(records, who)` | {void} | Refuse a corpus read whose records its caller did not supply. |
|
package/docs/content-format.md
CHANGED
|
@@ -1860,15 +1860,16 @@ immediately after the closing `]]`, with no space:
|
|
|
1860
1860
|
|
|
1861
1861
|
```markdown
|
|
1862
1862
|
![[branwldrgr|Brànwâal Dôrgaar]]{float: top-left}
|
|
1863
|
+
![[branwldrgr|Brànwâal Dôrgaar]]{size: medium, float: top-left}
|
|
1863
1864
|
![[thalornamap|Map of Thalorna]]{.full-width}
|
|
1864
1865
|
![[thalornamap|Map of Thalorna]]{.full-width, float: top-left}
|
|
1865
1866
|
![[thalornamap|Map of Thalorna]]{float: top-left, .full-width}
|
|
1866
1867
|
```
|
|
1867
1868
|
|
|
1868
|
-
**The same parser reads both**, so an embed takes a width class, a `
|
|
1869
|
-
|
|
1869
|
+
**The same parser reads both**, so an embed takes a width class, a `size:`, a
|
|
1870
|
+
`float:`, or a combination in any order, comma-separated — everything
|
|
1870
1871
|
[an image's directive](#width-is-a-class-and-the-ordinary-width-carries-no-marker)
|
|
1871
|
-
takes and nothing beyond it. The width and position vocabularies are the ones
|
|
1872
|
+
takes and nothing beyond it. The width, size, and position vocabularies are the ones
|
|
1872
1873
|
above, and they are closed here for the same reason: a directive that is quietly
|
|
1873
1874
|
ignored looks exactly like one that worked, and a directive holding a problem is
|
|
1874
1875
|
not honoured in part either.
|
|
@@ -2123,6 +2124,7 @@ part of this format follows.
|
|
|
2123
2124
|
|
|
2124
2125
|
```markdown
|
|
2125
2126
|
![[branwldrgr|Brànwâal Dôrgaar]]{float: top-left}
|
|
2127
|
+
![[branwldrgr|Brànwâal Dôrgaar]]{size: medium, float: top-left}
|
|
2126
2128
|
|
|
2127
2129
|
{.full-width}
|
|
2128
2130
|
```
|
|
@@ -2146,13 +2148,30 @@ place for those words and this is it.
|
|
|
2146
2148
|
| `.full-width` | the full page | full content width | full page width |
|
|
2147
2149
|
|
|
2148
2150
|
The simple case needs no spelling, which is why the ordinary width has no name.
|
|
2149
|
-
A third width joins this table as a name.
|
|
2150
2151
|
|
|
2151
2152
|
**No pixel values, ever.** A number means something in a browser and nothing
|
|
2152
2153
|
coherent in print, and a directive carrying a class and a dimension at once
|
|
2153
2154
|
gives one question two answers with no rule for which wins. `{width=800}` is
|
|
2154
2155
|
refused.
|
|
2155
2156
|
|
|
2157
|
+
##### Named size is `size:`
|
|
2158
|
+
|
|
2159
|
+
| `size` value | display |
|
|
2160
|
+
| ------------ | ------------------------ |
|
|
2161
|
+
| `auto` | same as omitting `size:` |
|
|
2162
|
+
| `small` | same as omitting `size:` |
|
|
2163
|
+
| `medium` | same as omitting `size:` |
|
|
2164
|
+
| `large` | same as omitting `size:` |
|
|
2165
|
+
| `xlarge` | same as omitting `size:` |
|
|
2166
|
+
| `full-width` | same as omitting `size:` |
|
|
2167
|
+
|
|
2168
|
+
The parser accepts and records these names for Markdown images and embedded
|
|
2169
|
+
assets. **A `size:` value does not change the displayed dimensions** on the
|
|
2170
|
+
website, in Foundry, or in the book. With no `size:`, the parser records `auto`;
|
|
2171
|
+
the existing rendering rules apply. In particular, book images occupy a column
|
|
2172
|
+
unless they carry `.full-width`. A `size:` may sit before or after `float:` in
|
|
2173
|
+
the same directive, and neither changes the other's behavior.
|
|
2174
|
+
|
|
2156
2175
|
##### Position is `float:`
|
|
2157
2176
|
|
|
2158
2177
|
| `float` value | book | website and Foundry journal |
|
|
@@ -2182,7 +2201,7 @@ A `.full-width` image that **also states a `float:`** is asking for a float and
|
|
|
2182
2201
|
gets one, with no break before it. A float is placed where it fits, which may be
|
|
2183
2202
|
the next page.
|
|
2184
2203
|
|
|
2185
|
-
#####
|
|
2204
|
+
##### All three vocabularies are closed
|
|
2186
2205
|
|
|
2187
2206
|
`{.fullwidth}`, `{.full_width}`, `{width=800}` and `{float: middle}` are
|
|
2188
2207
|
refused, located by file, line and column, and the build fails. An unrecognised
|
|
@@ -2194,8 +2213,8 @@ even in the part that parsed: a half-honoured directive is the same silent
|
|
|
2194
2213
|
failure in a smaller costume. The image keeps its braces and renders them as
|
|
2195
2214
|
literal text, so the mistake is visible on the page as well as in the log.
|
|
2196
2215
|
|
|
2197
|
-
**Nothing else reaches emitted markup.** The
|
|
2198
|
-
are all that leave a note; there is no `style`, no `id` and no arbitrary
|
|
2216
|
+
**Nothing else reaches emitted markup.** The width and position classes and the
|
|
2217
|
+
address are all that leave a note; there is no `style`, no `id` and no arbitrary
|
|
2199
2218
|
attribute, because data is never compiled into markup here. An address carries
|
|
2200
2219
|
a scheme of `http:` or `https:`, or none at all.
|
|
2201
2220
|
|
|
@@ -2399,15 +2418,17 @@ This is secret text
|
|
|
2399
2418
|
:::
|
|
2400
2419
|
```
|
|
2401
2420
|
|
|
2402
|
-
|
|
2421
|
+
Use a whole-line `:::secret` opener and a whole-line `:::` closer to mark a GM passage. Markdown inside the block, including links and emphasis, renders normally. Keep the fences outside code blocks and close each one before starting another. An unclosed or nested block is a build error at the source line.
|
|
2422
|
+
|
|
2423
|
+
In FoundryVTT, the passage is a secret section visible to the GM and owners of the journal:
|
|
2403
2424
|
|
|
2404
2425
|
```html
|
|
2405
|
-
<section class="secret">
|
|
2426
|
+
<section class="secret" id="secret-example">
|
|
2406
2427
|
<p>This is secret text</p>
|
|
2407
2428
|
</section>
|
|
2408
2429
|
```
|
|
2409
2430
|
|
|
2410
|
-
On webpages,
|
|
2431
|
+
On webpages, the passage appears as a collapsed spoiler that any reader can open:
|
|
2411
2432
|
|
|
2412
2433
|
```html
|
|
2413
2434
|
<details>
|
|
@@ -2416,6 +2437,8 @@ On webpages, this will be achieved with the use of
|
|
|
2416
2437
|
</details>
|
|
2417
2438
|
```
|
|
2418
2439
|
|
|
2440
|
+
In books, the passage is visible under a **GM note** label. A printed page has no reader permissions or interactive disclosure control.
|
|
2441
|
+
|
|
2419
2442
|
#### Actors
|
|
2420
2443
|
|
|
2421
2444
|
All actor types, including being and vehicle, have their entire markdown section processed as normal.
|
package/docs/getting-started.md
CHANGED
|
@@ -332,6 +332,18 @@ an error. A `data:` block and a `sohl:` / `hm3:` block are closed: a misspelled
|
|
|
332
332
|
key there is a finding that names the key you meant.
|
|
333
333
|
[`content-format.md`](content-format.md) is the specification.
|
|
334
334
|
|
|
335
|
+
An image stands in its own paragraph. Its directive accepts a named size and a
|
|
336
|
+
position together:
|
|
337
|
+
|
|
338
|
+
```markdown
|
|
339
|
+
![[icon-anubis|Anubis]]{size: medium, float: top-left}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The accepted sizes are `auto`, `small`, `medium`, `large`, `xlarge`, and
|
|
343
|
+
`full-width`. Omitting `size:` means `auto`. The build checks the name, but
|
|
344
|
+
`size:` does not change the displayed dimensions on the website, in Foundry, or
|
|
345
|
+
in the book. See [Images](content-format.md#images) for the rendering rules.
|
|
346
|
+
|
|
335
347
|
## Step 6 — check the tree
|
|
336
348
|
|
|
337
349
|
Four checks, and they are quick enough to run continuously while authoring.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { renderFoundryMarkdown } from "./helpers.mjs";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Extract the body of an H1 section whose heading carries the explicit
|
|
@@ -79,5 +79,5 @@ export function extractAnchorSection(body, anchorId) {
|
|
|
79
79
|
*/
|
|
80
80
|
export function renderSection(body, anchorId) {
|
|
81
81
|
const slice = extractAnchorSection(body, anchorId);
|
|
82
|
-
return slice ?
|
|
82
|
+
return slice ? renderFoundryMarkdown(slice) : "";
|
|
83
83
|
}
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
64
|
import { authoredFrontmatter } from "./index-records.mjs";
|
|
65
|
+
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
65
66
|
import { cloneAddressState } from "./address-values.mjs";
|
|
66
67
|
import fs from "fs";
|
|
67
68
|
import path from "path";
|
|
@@ -620,6 +621,18 @@ export class BasePackCompiler {
|
|
|
620
621
|
* that does not convert.
|
|
621
622
|
*/
|
|
622
623
|
convertBody(fm, body) {
|
|
624
|
+
const secretError = renderSecretBlocks(body, "book").errors[0];
|
|
625
|
+
if (secretError) {
|
|
626
|
+
const error = new Error(secretError.message);
|
|
627
|
+
error.position = {
|
|
628
|
+
line: (this.currentNote?.bodyLine ?? 1) + secretError.line - 1,
|
|
629
|
+
column:
|
|
630
|
+
secretError.line === 1 ?
|
|
631
|
+
(this.currentNote?.bodyColumn ?? 1)
|
|
632
|
+
: secretError.column,
|
|
633
|
+
};
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
623
636
|
if (!this.constructor.convertsWikilinks) return body;
|
|
624
637
|
const name = resolveName(fm);
|
|
625
638
|
const { absPath, bodyLine, bodyColumn } = this.currentNote ?? {};
|
package/engine/bundles.mjs
CHANGED
|
@@ -72,7 +72,7 @@ import { contentPackage } from "./content-package.mjs";
|
|
|
72
72
|
import { FOLDER_TYPE } from "./folder-notes.mjs";
|
|
73
73
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
74
74
|
import { itemDocEntryId } from "./item-docs.mjs";
|
|
75
|
-
import { folderField,
|
|
75
|
+
import { folderField, renderFoundryMarkdown, resolveName } from "./helpers.mjs";
|
|
76
76
|
import { packForType } from "./ids.mjs";
|
|
77
77
|
import { readQualifier } from "./wikilinks.mjs";
|
|
78
78
|
|
|
@@ -322,7 +322,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
322
322
|
// Foundry renders on the import card. That is why a bundle earns no
|
|
323
323
|
// separate documentation journal the way an item does: the document
|
|
324
324
|
// it compiles into already has somewhere to put the prose.
|
|
325
|
-
description: markdown.trim() ?
|
|
325
|
+
description: markdown.trim() ? renderFoundryMarkdown(markdown) : "",
|
|
326
326
|
folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
|
|
327
327
|
flags: fm.flags,
|
|
328
328
|
stats: this.stats,
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
* {float: top-left}
|
|
20
20
|
* {.full-width}
|
|
21
21
|
*
|
|
22
|
-
* ##
|
|
22
|
+
* ## Three closed vocabularies, and closed is the point
|
|
23
23
|
*
|
|
24
24
|
* **Width is a class**, and the ordinary width carries no marker at all — the
|
|
25
25
|
* simple case needs no spelling. {@link IMAGE_CLASSES} holds the one class
|
|
26
26
|
* there is. **Position is `float:`**, and {@link IMAGE_FLOATS} holds the five
|
|
27
|
-
* values it takes.
|
|
27
|
+
* values it takes. **Named size is `size:`**, and {@link IMAGE_SIZES} holds its
|
|
28
|
+
* values. Size is parsed but does not change the figure's current presentation.
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
+
* All three are closed, and an unrecognised value is **refused with a located
|
|
30
31
|
* diagnostic** rather than ignored. Ignoring is the failure worth preventing:
|
|
31
32
|
* `{.fullwidth}` rendering as an ordinary image looks exactly like a directive
|
|
32
33
|
* that worked, so the author publishes a page that is not the one they asked
|
|
@@ -36,8 +37,7 @@
|
|
|
36
37
|
*
|
|
37
38
|
* **No pixel values.** A number means something in a browser and nothing
|
|
38
39
|
* coherent in print, and a directive carrying both a class and a dimension
|
|
39
|
-
* gives one question two answers with no rule for which wins.
|
|
40
|
-
* joins the vocabulary as a name.
|
|
40
|
+
* gives one question two answers with no rule for which wins.
|
|
41
41
|
*
|
|
42
42
|
* **Not `markdown-it-attrs`.** It is a general attribute injector: it will set
|
|
43
43
|
* `style`, `id` or `onclick`, and those reach emitted markup on the website and
|
|
@@ -105,6 +105,16 @@ export const IMAGE_CLASSES = Object.freeze({
|
|
|
105
105
|
},
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
/** The named sizes an image directive accepts without applying a display size. */
|
|
109
|
+
export const IMAGE_SIZES = Object.freeze([
|
|
110
|
+
"auto",
|
|
111
|
+
"small",
|
|
112
|
+
"medium",
|
|
113
|
+
"large",
|
|
114
|
+
"xlarge",
|
|
115
|
+
"full-width",
|
|
116
|
+
]);
|
|
117
|
+
|
|
108
118
|
/**
|
|
109
119
|
* The `float:` positions an image may take, and where each puts it.
|
|
110
120
|
*
|
|
@@ -213,16 +223,18 @@ export function imageSourceProblem(src) {
|
|
|
213
223
|
*
|
|
214
224
|
* Values are validated here rather than at the point of rendering, so a mistake
|
|
215
225
|
* is one finding with a position rather than three surfaces quietly drawing
|
|
216
|
-
* something else. A directive that holds a problem yields **no** class
|
|
226
|
+
* something else. A directive that holds a problem yields **no** class, size or
|
|
217
227
|
* float: a half-honoured directive is the silent failure in a smaller costume.
|
|
218
228
|
*
|
|
219
229
|
* @param {string} [raw] - The text between the braces, braces included or not.
|
|
220
|
-
* @returns {{classes: string[], float: string, problems: string[]}} What was
|
|
230
|
+
* @returns {{classes: string[], size: string, float: string, problems: string[]}} What was
|
|
221
231
|
* written, and what cannot be honoured.
|
|
222
232
|
*/
|
|
223
233
|
export function parseImageDirective(raw) {
|
|
224
234
|
/** @type {string[]} */
|
|
225
235
|
const classes = [];
|
|
236
|
+
let size = "auto";
|
|
237
|
+
let sizeSeen = false;
|
|
226
238
|
let float = "";
|
|
227
239
|
/** @type {string[]} */
|
|
228
240
|
const problems = [];
|
|
@@ -230,7 +242,7 @@ export function parseImageDirective(raw) {
|
|
|
230
242
|
const inner = String(raw ?? "")
|
|
231
243
|
.replace(/^\{/, "")
|
|
232
244
|
.replace(/\}$/, "");
|
|
233
|
-
if (!inner.trim()) return { classes, float, problems };
|
|
245
|
+
if (!inner.trim()) return { classes, size, float, problems };
|
|
234
246
|
|
|
235
247
|
// Split on commas, not whitespace: `float: top-left` is one pair with a
|
|
236
248
|
// space in it, and the space after the colon is the spelling people write.
|
|
@@ -259,17 +271,32 @@ export function parseImageDirective(raw) {
|
|
|
259
271
|
const colon = part.indexOf(":");
|
|
260
272
|
if (colon === -1) {
|
|
261
273
|
problems.push(
|
|
262
|
-
`\`${part}\` is neither a width class nor \`
|
|
263
|
-
"image states its width as a class and its position as `float:`, " +
|
|
274
|
+
`\`${part}\` is neither a width class nor \`size: <name>\` or ` +
|
|
275
|
+
"`float: <position>` — an image states its width as a class and its position as `float:`, " +
|
|
264
276
|
"and it states no dimensions at all",
|
|
265
277
|
);
|
|
266
278
|
continue;
|
|
267
279
|
}
|
|
268
280
|
const key = part.slice(0, colon).trim();
|
|
269
281
|
const value = part.slice(colon + 1).trim();
|
|
282
|
+
if (key === "size") {
|
|
283
|
+
if (!IMAGE_SIZES.includes(value)) {
|
|
284
|
+
problems.push(
|
|
285
|
+
`\`size: ${value}\` is not a size — the ones there are: ${IMAGE_SIZES.join(", ")}`,
|
|
286
|
+
);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (sizeSeen) {
|
|
290
|
+
problems.push("`size:` is written twice, and an image has one named size");
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
size = value;
|
|
294
|
+
sizeSeen = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
270
297
|
if (key !== "float") {
|
|
271
298
|
problems.push(
|
|
272
|
-
`\`${key}\` is not an image attribute — \`
|
|
299
|
+
`\`${key}\` is not an image attribute — use \`size\` or \`float\`, ` +
|
|
273
300
|
"and width is a class rather than an attribute",
|
|
274
301
|
);
|
|
275
302
|
continue;
|
|
@@ -292,8 +319,8 @@ export function parseImageDirective(raw) {
|
|
|
292
319
|
problems.push("an image states one width, and this states more than one");
|
|
293
320
|
}
|
|
294
321
|
// Nothing partial: a directive with a problem in it is not honoured at all.
|
|
295
|
-
if (problems.length) return { classes: [], float: "", problems };
|
|
296
|
-
return { classes, float, problems };
|
|
322
|
+
if (problems.length) return { classes: [], size: "auto", float: "", problems };
|
|
323
|
+
return { classes, size, float, problems };
|
|
297
324
|
}
|
|
298
325
|
|
|
299
326
|
/**
|
|
@@ -594,7 +621,7 @@ export function lintContentImages(contentBase, { skipDirectories = [], config }
|
|
|
594
621
|
* **Hugo is handed markdown, not a rendered page.** The site emitter writes a
|
|
595
622
|
* note's body through verbatim, so a `{…}` directive left in it reaches the
|
|
596
623
|
* page as its own literal braces: Goldmark's block-attribute parser is off, and
|
|
597
|
-
* turning it on would accept `style` and `id` alongside the
|
|
624
|
+
* turning it on would accept `style` and `id` alongside the closed vocabularies,
|
|
598
625
|
* which is the injection surface this rule exists to avoid. So the directive is
|
|
599
626
|
* resolved here, into markup Goldmark passes through.
|
|
600
627
|
*
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import markdownit from "markdown-it";
|
|
7
|
+
|
|
8
|
+
const webMarkdown = markdownit({ html: true });
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render whole-line `:::secret` blocks for one publishing surface.
|
|
12
|
+
* The source line of each syntax error is relative to the supplied body.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} source - Markdown containing secret blocks.
|
|
15
|
+
* @param {"foundry"|"web"|"book"} target - Publishing surface.
|
|
16
|
+
* @param {(markdown: string) => string} [renderMarkdown] - Foundry's configured renderer.
|
|
17
|
+
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
|
|
18
|
+
*/
|
|
19
|
+
export function renderSecretBlocks(
|
|
20
|
+
source,
|
|
21
|
+
target,
|
|
22
|
+
renderMarkdown = webMarkdown.render.bind(webMarkdown),
|
|
23
|
+
) {
|
|
24
|
+
const lines = String(source ?? "").split("\n");
|
|
25
|
+
const result = [];
|
|
26
|
+
const errors = [];
|
|
27
|
+
let opening = -1;
|
|
28
|
+
let body = [];
|
|
29
|
+
let codeFence = null;
|
|
30
|
+
|
|
31
|
+
for (let index = 0; index < lines.length; index++) {
|
|
32
|
+
const line = lines[index];
|
|
33
|
+
const fence = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
34
|
+
if (codeFence) {
|
|
35
|
+
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
|
|
36
|
+
codeFence = null;
|
|
37
|
+
}
|
|
38
|
+
if (opening >= 0) body.push(line);
|
|
39
|
+
else result.push(line);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (fence) {
|
|
43
|
+
codeFence = fence[1];
|
|
44
|
+
if (opening >= 0) body.push(line);
|
|
45
|
+
else result.push(line);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/^:::secret[ \t]*$/.test(line)) {
|
|
49
|
+
if (opening >= 0) {
|
|
50
|
+
errors.push({
|
|
51
|
+
line: index + 1,
|
|
52
|
+
column: 1,
|
|
53
|
+
message: "nested secret blocks are not supported",
|
|
54
|
+
});
|
|
55
|
+
body.push(line);
|
|
56
|
+
} else {
|
|
57
|
+
opening = index;
|
|
58
|
+
body = [];
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (/^:::[ \t]*$/.test(line)) {
|
|
63
|
+
if (opening < 0) {
|
|
64
|
+
errors.push({
|
|
65
|
+
line: index + 1,
|
|
66
|
+
column: 1,
|
|
67
|
+
message: "secret block has no opening :::secret line",
|
|
68
|
+
});
|
|
69
|
+
result.push(line);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const inner = body.join("\n").trim();
|
|
73
|
+
if (target === "book") {
|
|
74
|
+
result.push("", "**GM note**", "", inner, "");
|
|
75
|
+
} else {
|
|
76
|
+
const html = renderMarkdown(inner).trim();
|
|
77
|
+
if (target === "foundry") {
|
|
78
|
+
const id = crypto
|
|
79
|
+
.createHash("sha256")
|
|
80
|
+
.update(`${opening}:${inner}`)
|
|
81
|
+
.digest("hex")
|
|
82
|
+
.slice(0, 12);
|
|
83
|
+
result.push(
|
|
84
|
+
"",
|
|
85
|
+
`<section class="secret" id="secret-${id}">`,
|
|
86
|
+
html,
|
|
87
|
+
"</section>",
|
|
88
|
+
"",
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
result.push("", "<details><summary>Spoiler</summary>", html, "</details>", "");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
opening = -1;
|
|
95
|
+
body = [];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (opening >= 0) body.push(line);
|
|
99
|
+
else result.push(line);
|
|
100
|
+
}
|
|
101
|
+
if (opening >= 0) {
|
|
102
|
+
errors.push({
|
|
103
|
+
line: opening + 1,
|
|
104
|
+
column: 1,
|
|
105
|
+
message: "secret block needs a closing ::: line",
|
|
106
|
+
});
|
|
107
|
+
result.push(":::secret", ...body);
|
|
108
|
+
}
|
|
109
|
+
return { markdown: result.join("\n"), errors };
|
|
110
|
+
}
|
package/engine/helpers.mjs
CHANGED
|
@@ -62,6 +62,7 @@ import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
|
62
62
|
// from there rather than respelt, so the tag and its one reader cannot drift.
|
|
63
63
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
64
64
|
import { expandContentTables } from "./content-tables.mjs";
|
|
65
|
+
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
65
66
|
import { positionInBody } from "./diagnostics.mjs";
|
|
66
67
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
67
68
|
// registry can import them without reaching back through this one.
|
|
@@ -125,6 +126,12 @@ export const md = markdownit({ html: true })
|
|
|
125
126
|
}),
|
|
126
127
|
);
|
|
127
128
|
|
|
129
|
+
/** Render a note body with Foundry's secret section markup. */
|
|
130
|
+
export function renderFoundryMarkdown(body) {
|
|
131
|
+
const { markdown } = renderSecretBlocks(body, "foundry", (inner) => md.render(inner));
|
|
132
|
+
return md.render(markdown);
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
/**
|
|
129
136
|
* Parses a markdown file with YAML frontmatter.
|
|
130
137
|
*
|
|
@@ -165,7 +172,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
165
172
|
}
|
|
166
173
|
const raw = fmMatch[2];
|
|
167
174
|
const body = raw.trim();
|
|
168
|
-
const description = body ?
|
|
175
|
+
const description = body ? renderFoundryMarkdown(body) : "";
|
|
169
176
|
// Where the trimmed body starts in the *file*, so an offset within it can
|
|
170
177
|
// be reported as a file position. The frontmatter's lines and the
|
|
171
178
|
// blank lines `trim()` removes both sit in between, and the trim can take
|
package/engine/journals.mjs
CHANGED
|
@@ -51,7 +51,14 @@
|
|
|
51
51
|
|
|
52
52
|
import log from "loglevel";
|
|
53
53
|
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
sohlField,
|
|
56
|
+
makeId,
|
|
57
|
+
resolveName,
|
|
58
|
+
defaultStats,
|
|
59
|
+
renderFoundryMarkdown,
|
|
60
|
+
folderField,
|
|
61
|
+
} from "./helpers.mjs";
|
|
55
62
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
56
63
|
import { anchorPageId, resolveReference } from "./wikilinks.mjs";
|
|
57
64
|
import { infoboxesToHtml, linkToUuid } from "./infobox-render.mjs";
|
|
@@ -83,6 +90,7 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
83
90
|
const beforeFirstH1 = [];
|
|
84
91
|
let current = null;
|
|
85
92
|
let inCodeBlock = false;
|
|
93
|
+
let inSecret = false;
|
|
86
94
|
|
|
87
95
|
const closeCurrent = () => {
|
|
88
96
|
if (!current) return;
|
|
@@ -99,11 +107,14 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
99
107
|
if (line.trim().startsWith("```")) {
|
|
100
108
|
inCodeBlock = !inCodeBlock;
|
|
101
109
|
}
|
|
110
|
+
if (!inCodeBlock && /^:::secret[ \t]*$/.test(line)) inSecret = true;
|
|
111
|
+
else if (!inCodeBlock && /^:::[ \t]*$/.test(line)) inSecret = false;
|
|
102
112
|
|
|
103
113
|
// An H1 starts a page, as does any heading carrying an `{#slug}`
|
|
104
114
|
// anchor: a Foundry UUID can only address a page, so a linkable
|
|
105
115
|
// section has to be one.
|
|
106
|
-
const headingMatch =
|
|
116
|
+
const headingMatch =
|
|
117
|
+
!inCodeBlock && !inSecret ? line.match(/^\s*(#{1,6})\s+(.+?)\s*#*\s*$/) : null;
|
|
107
118
|
const rawHeading = headingMatch?.[2]?.trim();
|
|
108
119
|
const anchorMatch = rawHeading?.match(/^(.*?)\s*\{#([^}]+)\}\s*$/);
|
|
109
120
|
const startsPage = headingMatch && (headingMatch[1].length === 1 || anchorMatch);
|
|
@@ -255,7 +266,7 @@ export function buildPages(rawPages, entryId, noteName) {
|
|
|
255
266
|
title: { show: true, level: page.level ?? 1 },
|
|
256
267
|
text: {
|
|
257
268
|
format: 1,
|
|
258
|
-
content: page.markdown ?
|
|
269
|
+
content: page.markdown ? renderFoundryMarkdown(page.markdown) : "",
|
|
259
270
|
},
|
|
260
271
|
_key: `!journal.pages!${entryId}.${pageId}`,
|
|
261
272
|
};
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -102,6 +102,7 @@ import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
|
102
102
|
import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
103
103
|
import { assetAddressIndex } from "./art-fields.mjs";
|
|
104
104
|
import { expandContentTables } from "./content-tables.mjs";
|
|
105
|
+
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
105
106
|
import { protectCode } from "./code-fences.mjs";
|
|
106
107
|
import { imageSourcesIn } from "./content-images.mjs";
|
|
107
108
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
@@ -523,8 +524,18 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
523
524
|
message: err.message ?? (err.reason ? linkFindingMessage(err) : String(err)),
|
|
524
525
|
});
|
|
525
526
|
}
|
|
526
|
-
|
|
527
|
-
const
|
|
527
|
+
const secrets = renderSecretBlocks(resolvedBody, "book");
|
|
528
|
+
for (const error of renderSecretBlocks(page.body, "book").errors) {
|
|
529
|
+
findings.push({
|
|
530
|
+
file: page.file,
|
|
531
|
+
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
532
|
+
column: error.column,
|
|
533
|
+
severity: "error",
|
|
534
|
+
message: error.message,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
stageImages(secrets.markdown, page.file);
|
|
538
|
+
const prose = markdownToTypst(secrets.markdown, {
|
|
528
539
|
md,
|
|
529
540
|
links: plan.links,
|
|
530
541
|
glyphs,
|
package/engine/site-build.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import matter from "gray-matter";
|
|
|
61
61
|
import { addressSlug } from "./content-address.mjs";
|
|
62
62
|
import { protectCode } from "./code-fences.mjs";
|
|
63
63
|
import { expandContentTables } from "./content-tables.mjs";
|
|
64
|
+
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
64
65
|
import { renderImageFigures } from "./content-images.mjs";
|
|
65
66
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
66
67
|
import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
@@ -140,8 +141,10 @@ function siteCorpusFiles(contentBase, ctx) {
|
|
|
140
141
|
*/
|
|
141
142
|
function readNote(file, ctx) {
|
|
142
143
|
let parsed;
|
|
144
|
+
let source;
|
|
143
145
|
try {
|
|
144
|
-
|
|
146
|
+
source = fs.readFileSync(file, "utf8");
|
|
147
|
+
parsed = matter(source);
|
|
145
148
|
} catch {
|
|
146
149
|
return null;
|
|
147
150
|
}
|
|
@@ -150,7 +153,11 @@ function readNote(file, ctx) {
|
|
|
150
153
|
package: ctx.contentPackage ?? ctx.config?.contentPackage,
|
|
151
154
|
};
|
|
152
155
|
try {
|
|
153
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
fm: decodeNoteAddresses(parsed.data, context),
|
|
158
|
+
body: parsed.content,
|
|
159
|
+
bodyLine: source.slice(0, source.length - parsed.content.length).split("\n").length,
|
|
160
|
+
};
|
|
154
161
|
} catch (error) {
|
|
155
162
|
const position = positionOfYamlPath(parsed.matter, error.keyPath ?? [], {
|
|
156
163
|
key: error.addressKey,
|
|
@@ -202,7 +209,7 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
202
209
|
const note = readNote(file, ctx);
|
|
203
210
|
if (!note) continue;
|
|
204
211
|
applyComputedBeingAge(note.fm, present);
|
|
205
|
-
const { fm, body } = note;
|
|
212
|
+
const { fm, body, bodyLine } = note;
|
|
206
213
|
// The configuration's, never a note's: `package:` is retired, so every
|
|
207
214
|
// note in the tree belongs to the package this repository compiles.
|
|
208
215
|
const pkg = ctx.contentPackage;
|
|
@@ -249,6 +256,7 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
249
256
|
// — reads one configured value and never frontmatter.
|
|
250
257
|
pkg,
|
|
251
258
|
body,
|
|
259
|
+
bodyLine,
|
|
252
260
|
name,
|
|
253
261
|
slug,
|
|
254
262
|
base,
|
|
@@ -734,6 +742,7 @@ export function renderPages(pages, options) {
|
|
|
734
742
|
});
|
|
735
743
|
|
|
736
744
|
const tableErrors = [];
|
|
745
|
+
const secretErrors = [];
|
|
737
746
|
const wikiErrors = [];
|
|
738
747
|
const imageErrors = [];
|
|
739
748
|
const byKind = {};
|
|
@@ -847,7 +856,17 @@ export function renderPages(pages, options) {
|
|
|
847
856
|
tableErrors.push(...errors);
|
|
848
857
|
|
|
849
858
|
const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
|
|
850
|
-
|
|
859
|
+
const resolvedBody = protectCode(body, resolve);
|
|
860
|
+
const secrets = renderSecretBlocks(resolvedBody, "web");
|
|
861
|
+
for (const error of renderSecretBlocks(page.body, "book").errors) {
|
|
862
|
+
secretErrors.push({
|
|
863
|
+
file: page.file,
|
|
864
|
+
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
865
|
+
column: error.column,
|
|
866
|
+
message: error.message,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
rendered.push({ page, body: secrets.markdown, data });
|
|
851
870
|
// A resolved target with no URL is a pack-only package's address:
|
|
852
871
|
// real, and nowhere on the web, so not an edge.
|
|
853
872
|
for (const hit of resolved) {
|
|
@@ -885,6 +904,7 @@ export function renderPages(pages, options) {
|
|
|
885
904
|
written: pages.length,
|
|
886
905
|
byKind,
|
|
887
906
|
tableErrors,
|
|
907
|
+
secretErrors,
|
|
888
908
|
wikiErrors,
|
|
889
909
|
imageErrors,
|
|
890
910
|
related,
|
|
@@ -1044,6 +1064,7 @@ export function buildSite({ config, sqlTables, locate } = {}) {
|
|
|
1044
1064
|
gates: { ...emptyGates(), homepages: homepageFindings },
|
|
1045
1065
|
manifests: null,
|
|
1046
1066
|
tableErrors: [],
|
|
1067
|
+
secretErrors: [],
|
|
1047
1068
|
wikiErrors: [],
|
|
1048
1069
|
imageErrors: [],
|
|
1049
1070
|
mapFindings: [],
|
|
@@ -1063,6 +1084,7 @@ export function buildSite({ config, sqlTables, locate } = {}) {
|
|
|
1063
1084
|
gates: emptyGates(),
|
|
1064
1085
|
manifests: null,
|
|
1065
1086
|
tableErrors: [],
|
|
1087
|
+
secretErrors: [],
|
|
1066
1088
|
wikiErrors: [],
|
|
1067
1089
|
imageErrors: [],
|
|
1068
1090
|
mapFindings: [],
|
|
@@ -1108,6 +1130,7 @@ export function buildSite({ config, sqlTables, locate } = {}) {
|
|
|
1108
1130
|
gates,
|
|
1109
1131
|
stats: null,
|
|
1110
1132
|
tableErrors: [],
|
|
1133
|
+
secretErrors: [],
|
|
1111
1134
|
wikiErrors: [],
|
|
1112
1135
|
imageErrors: [],
|
|
1113
1136
|
mapFindings: [],
|
|
@@ -1175,6 +1198,7 @@ export function buildSite({ config, sqlTables, locate } = {}) {
|
|
|
1175
1198
|
return {
|
|
1176
1199
|
gates,
|
|
1177
1200
|
tableErrors: rendered.tableErrors,
|
|
1201
|
+
secretErrors: rendered.secretErrors,
|
|
1178
1202
|
wikiErrors: rendered.wikiErrors,
|
|
1179
1203
|
imageErrors: rendered.imageErrors,
|
|
1180
1204
|
mapFindings: drawn.findings,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.12.
|
|
3
|
+
"version": "22.12.2",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -16,15 +16,16 @@ export function imageSourceProblem(src: string): string;
|
|
|
16
16
|
*
|
|
17
17
|
* Values are validated here rather than at the point of rendering, so a mistake
|
|
18
18
|
* is one finding with a position rather than three surfaces quietly drawing
|
|
19
|
-
* something else. A directive that holds a problem yields **no** class
|
|
19
|
+
* something else. A directive that holds a problem yields **no** class, size or
|
|
20
20
|
* float: a half-honoured directive is the silent failure in a smaller costume.
|
|
21
21
|
*
|
|
22
22
|
* @param {string} [raw] - The text between the braces, braces included or not.
|
|
23
|
-
* @returns {{classes: string[], float: string, problems: string[]}} What was
|
|
23
|
+
* @returns {{classes: string[], size: string, float: string, problems: string[]}} What was
|
|
24
24
|
* written, and what cannot be honoured.
|
|
25
25
|
*/
|
|
26
26
|
export function parseImageDirective(raw?: string): {
|
|
27
27
|
classes: string[];
|
|
28
|
+
size: string;
|
|
28
29
|
float: string;
|
|
29
30
|
problems: string[];
|
|
30
31
|
};
|
|
@@ -182,7 +183,7 @@ export function lintContentImages(contentBase: string, { skipDirectories, config
|
|
|
182
183
|
* **Hugo is handed markdown, not a rendered page.** The site emitter writes a
|
|
183
184
|
* note's body through verbatim, so a `{…}` directive left in it reaches the
|
|
184
185
|
* page as its own literal braces: Goldmark's block-attribute parser is off, and
|
|
185
|
-
* turning it on would accept `style` and `id` alongside the
|
|
186
|
+
* turning it on would accept `style` and `id` alongside the closed vocabularies,
|
|
186
187
|
* which is the injection surface this rule exists to avoid. So the directive is
|
|
187
188
|
* resolved here, into markup Goldmark passes through.
|
|
188
189
|
*
|
|
@@ -248,6 +249,8 @@ export const IMAGE_CLASSES: Readonly<Record<string, {
|
|
|
248
249
|
scope: string;
|
|
249
250
|
describe: string;
|
|
250
251
|
}>>;
|
|
252
|
+
/** The named sizes an image directive accepts without applying a display size. */
|
|
253
|
+
export const IMAGE_SIZES: readonly string[];
|
|
251
254
|
/**
|
|
252
255
|
* The `float:` positions an image may take, and where each puts it.
|
|
253
256
|
*
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render whole-line `:::secret` blocks for one publishing surface.
|
|
3
|
+
* The source line of each syntax error is relative to the supplied body.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} source - Markdown containing secret blocks.
|
|
6
|
+
* @param {"foundry"|"web"|"book"} target - Publishing surface.
|
|
7
|
+
* @param {(markdown: string) => string} [renderMarkdown] - Foundry's configured renderer.
|
|
8
|
+
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
|
|
9
|
+
*/
|
|
10
|
+
export function renderSecretBlocks(source: string, target: "foundry" | "web" | "book", renderMarkdown?: (markdown: string) => string): {
|
|
11
|
+
markdown: string;
|
|
12
|
+
errors: Array<{
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}>;
|
|
17
|
+
};
|