docpensieve 0.5.0-beta.2 → 0.5.0-beta.3

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.
@@ -63,6 +63,7 @@ program
63
63
  .command('dev')
64
64
  .description('Development server with reload')
65
65
  .option('-p, --port <number>', 'listening port', Number, 3000)
66
+ .option('--host <address>', 'address to listen on; 0.0.0.0 opens it to the network', '127.0.0.1')
66
67
  .action(async (options) => {
67
68
  await dev(options);
68
69
  });
@@ -71,6 +72,7 @@ program
71
72
  .command('serve')
72
73
  .description('Serves the output folder statically')
73
74
  .option('-p, --port <number>', 'listening port', Number, 4000)
75
+ .option('--host <address>', 'address to listen on; 0.0.0.0 opens it to the network', '127.0.0.1')
74
76
  .option('-d, --dir <dir>', 'folder to serve')
75
77
  .action(async (options) => {
76
78
  await serve(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docpensieve",
3
- "version": "0.5.0-beta.2",
3
+ "version": "0.5.0-beta.3",
4
4
  "description": "DocPensieve command-line interface (init, build, check, dev, serve)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,10 +24,10 @@
24
24
  "types"
25
25
  ],
26
26
  "dependencies": {
27
- "@docpensieve/components": "0.5.0-beta.2",
28
- "@docpensieve/core": "0.5.0-beta.2",
29
- "@docpensieve/shared": "0.5.0-beta.2",
30
- "@docpensieve/theme": "0.5.0-beta.2",
27
+ "@docpensieve/components": "0.5.0-beta.3",
28
+ "@docpensieve/core": "0.5.0-beta.3",
29
+ "@docpensieve/shared": "0.5.0-beta.3",
30
+ "@docpensieve/theme": "0.5.0-beta.3",
31
31
  "chalk": "^6.0.0",
32
32
  "chokidar": "^5.0.0",
33
33
  "commander": "^15.0.0"
@@ -11,6 +11,7 @@ import {
11
11
  createRegistry,
12
12
  setSiteContext,
13
13
  setAdmonitionKinds,
14
+ setSnippetIcons,
14
15
  } from '@docpensieve/components';
15
16
  import { SiteGenerator, loadConfig, resolveVersion } from '@docpensieve/core';
16
17
 
@@ -28,6 +29,7 @@ export async function build(versionSlug, options = {}) {
28
29
 
29
30
  // The kinds a project declares, beside the ones shipped.
30
31
  setAdmonitionKinds(config.admonitions);
32
+ setSnippetIcons(config.snippetIcons);
31
33
 
32
34
  const generator = new SiteGenerator(config, {
33
35
  // `globalComponents: false` removes the shipped components: a project that
@@ -12,12 +12,20 @@ import {
12
12
  createRegistry,
13
13
  setSiteContext,
14
14
  setAdmonitionKinds,
15
+ setSnippetIcons,
15
16
  } from '@docpensieve/components';
16
17
  import { SiteGenerator, loadConfig } from '@docpensieve/core';
17
18
  import { CONFIG_FILENAME, DocPensieveError, THEME_FOLDER } from '@docpensieve/shared';
18
19
  import chokidar from 'chokidar';
19
20
 
20
- import { RELOAD_PATH, createStaticServer, listen } from '../server.js';
21
+ import {
22
+ DEFAULT_HOST,
23
+ RELOAD_PATH,
24
+ createStaticServer,
25
+ exposedNotice,
26
+ isExposed,
27
+ listen,
28
+ } from '../server.js';
21
29
  import { createTheme } from '../theme.js';
22
30
 
23
31
  /** Default port of `dev`, distinct from that of `serve`. */
@@ -40,7 +48,7 @@ const RELOAD_SCRIPT =
40
48
  `.addEventListener("message",()=>location.reload())</script>`;
41
49
 
42
50
  /**
43
- * @param {{ port?: number, cwd?: string }} [options]
51
+ * @param {{ port?: number, host?: string, cwd?: string }} [options]
44
52
  * @returns {Promise<{
45
53
  * server: import('node:http').Server,
46
54
  * watcher: import('chokidar').FSWatcher,
@@ -62,6 +70,7 @@ export async function dev(options = {}) {
62
70
  // Read again with the rest: a kind added to the configuration must show
63
71
  // without restarting the command.
64
72
  setAdmonitionKinds(current.admonitions);
73
+ setSnippetIcons(current.snippetIcons);
65
74
  const generator = new SiteGenerator(current, {
66
75
  // `globalComponents: false` removes the shipped components: a project
67
76
  // that defines its own thus avoids a name collision. The option was
@@ -87,8 +96,10 @@ export async function dev(options = {}) {
87
96
  },
88
97
  });
89
98
 
90
- const port = await listen(server, options.port ?? DEFAULT_PORT);
99
+ const host = options.host ?? DEFAULT_HOST;
100
+ const port = await listen(server, options.port ?? DEFAULT_PORT, 10, host);
91
101
  const url = `http://localhost:${port}${config.baseUrl}`;
102
+ if (isExposed(host)) console.log(exposedNotice(host));
92
103
  console.log(`served at ${url}`);
93
104
 
94
105
  // The project's own stylesheets, re-read by every rebuild.
@@ -368,6 +368,16 @@ function renderConfig({ name, theme, siteUrl, version, translation = '' }) {
368
368
  ' // Back-to-top button on every page.',
369
369
  ' scrollToTop: true,',
370
370
  '',
371
+ ' // A button copying each block of code. It is the one thing that makes a',
372
+ ' // content page load a script — a small file, shared by every page of the',
373
+ ' // version — so it stays off until you ask for it.',
374
+ ' copyCode: false,',
375
+ '',
376
+ ' // Icon collection a code block takes the mark of its language from, for',
377
+ " // instance 'simple-icons' once @iconify-json/simple-icons is installed.",
378
+ ' // Left empty, the short name of the language is shown instead.',
379
+ " snippetIcons: '',",
380
+ '',
371
381
  ' // The header stays at the top of the screen. false lets it scroll away',
372
382
  ' // with the page, and gives its height back to the text.',
373
383
  ' stickyHeader: true,',
@@ -10,13 +10,13 @@ import path from 'node:path';
10
10
  import { loadConfig } from '@docpensieve/core';
11
11
  import { DocPensieveError } from '@docpensieve/shared';
12
12
 
13
- import { createStaticServer, listen } from '../server.js';
13
+ import { DEFAULT_HOST, createStaticServer, exposedNotice, isExposed, listen } from '../server.js';
14
14
 
15
15
  /** Default port of `serve`, distinct from that of `dev`. */
16
16
  const DEFAULT_PORT = 4000;
17
17
 
18
18
  /**
19
- * @param {{ port?: number, dir?: string, cwd?: string }} [options]
19
+ * @param {{ port?: number, host?: string, dir?: string, cwd?: string }} [options]
20
20
  * @returns {Promise<{ server: import('node:http').Server, port: number, url: string }>}
21
21
  * @throws {DocPensieveError} When the folder to serve does not exist.
22
22
  */
@@ -34,11 +34,13 @@ export async function serve(options = {}) {
34
34
  // The site is mounted under the configuration's baseUrl: otherwise the links
35
35
  // of the generated pages would not resolve locally.
36
36
  const server = createStaticServer({ root, basePath: config.baseUrl });
37
- const port = await listen(server, options.port ?? DEFAULT_PORT);
37
+ const host = options.host ?? DEFAULT_HOST;
38
+ const port = await listen(server, options.port ?? DEFAULT_PORT, 10, host);
38
39
  const url = `http://localhost:${port}${config.baseUrl}`;
39
40
 
40
41
  console.log(`${root}`);
41
42
  console.log(`served at ${url}`);
43
+ if (isExposed(host)) console.log(exposedNotice(host));
42
44
 
43
45
  return { server, port, url };
44
46
  }
package/src/server.js CHANGED
@@ -162,16 +162,35 @@ export function createStaticServer({ root, basePath = '/', inject = null, onRelo
162
162
  });
163
163
  }
164
164
 
165
+ /** The loopback address: the site is served to this machine and to no other. */
166
+ export const DEFAULT_HOST = '127.0.0.1';
167
+
168
+ /**
169
+ * Whether an address reaches beyond this machine.
170
+ *
171
+ * @param {string} host
172
+ * @returns {boolean}
173
+ */
174
+ export function isExposed(host) {
175
+ return !['127.0.0.1', 'localhost', '::1'].includes(host);
176
+ }
177
+
165
178
  /**
166
179
  * Starts listening, looking for a free port if needed.
167
180
  *
181
+ * On this machine only, unless asked otherwise. Called without a host, Node
182
+ * listens on every interface: anyone on the same network — a café, a
183
+ * coworking space — could read the site being written, drafts included.
184
+ *
168
185
  * @param {import('node:http').Server} server
169
186
  * @param {number} port Desired port.
170
187
  * @param {number} [attempts] Number of ports tried from `port` on.
188
+ * @param {string} [host] Address to listen on. `0.0.0.0` opens it to the
189
+ * network, which a phone on the same wifi needs.
171
190
  * @returns {Promise<number>} The port actually used.
172
191
  * @throws {DocPensieveError} When no port is free in the range.
173
192
  */
174
- export function listen(server, port, attempts = 10) {
193
+ export function listen(server, port, attempts = 10, host = DEFAULT_HOST) {
175
194
  return new Promise((resolve, reject) => {
176
195
  let current = port;
177
196
 
@@ -181,7 +200,7 @@ export function listen(server, port, attempts = 10) {
181
200
  // the next one beats forcing --port.
182
201
  if (error.code === 'EADDRINUSE' && current < port + attempts - 1) {
183
202
  current += 1;
184
- server.listen(current);
203
+ server.listen(current, host);
185
204
  return;
186
205
  }
187
206
  server.off('error', onError);
@@ -202,6 +221,17 @@ export function listen(server, port, attempts = 10) {
202
221
  const address = server.address();
203
222
  resolve(typeof address === 'object' && address !== null ? address.port : current);
204
223
  });
205
- server.listen(current);
224
+ server.listen(current, host);
206
225
  });
207
226
  }
227
+
228
+ /**
229
+ * Said when the site is opened to the network, so that it is never done
230
+ * without the person knowing.
231
+ *
232
+ * @param {string} host
233
+ * @returns {string}
234
+ */
235
+ export function exposedNotice(host) {
236
+ return `listening on ${host}: anyone on this network can read the site.`;
237
+ }
@@ -98,6 +98,10 @@ online.
98
98
  → ignores the deployment prefix "/my-project/"
99
99
  ```
100
100
 
101
+ Two commands, and the second reads what the first wrote — so they belong
102
+ together. [Deployment](./deployment/) shows how to say that once, in a
103
+ `package.json`, and have a workflow run the same thing you run.
104
+
101
105
  It is the check to run after any change of `baseUrl`, of folder structure or of
102
106
  URL. When something does not add up, [When it breaks](./when-it-breaks/) lists
103
107
  the failures this tool produces, by the symptom you see.
@@ -88,6 +88,32 @@ the tool through `npx`, which takes the latest published version. To pin it
88
88
  from one build to the next, declare `docpensieve` in a `package.json` and add
89
89
  `npm ci` before the build.
90
90
 
91
+ ## One command, for you and for the machine
92
+
93
+ Two commands are one too many to remember, and a workflow should run exactly
94
+ what you run. A `package.json` is the place to say it once:
95
+
96
+ ```json
97
+ {
98
+ "scripts": {
99
+ "docs": "docpensieve dev",
100
+ "check": "docpensieve build && docpensieve check"
101
+ }
102
+ }
103
+ ```
104
+
105
+ `npm run check` then generates the site and reads it back. A dead link, a
106
+ nesting the markup cannot carry, a page that stops the build: each one fails
107
+ the command, on your machine and in your workflow, in the same way and with
108
+ the same message.
109
+
110
+ The `&&` is the point of it. `check` reads what `build` wrote, so a check run
111
+ on its own tells you about the site as it was the last time it was generated —
112
+ which, the day you forget, is the day it matters.
113
+
114
+ With that in place the recipe above becomes `npm ci` and `npm run check`, and
115
+ the version of the tool stops depending on the day the workflow ran.
116
+
91
117
  ## Reading back before publishing
92
118
 
93
119
  A generated site can compile without error and contain dead links.
@@ -31,8 +31,23 @@ npx docpensieve check
31
31
 
32
32
  ## What changes on its own
33
33
 
34
- Nothing yet: the 0.5 has just opened. Each change is listed here as it lands,
35
- beside what it asks of a project already built on the 0.4.
34
+ - **`dev` and `serve` listen on this machine only.** They used to listen on
35
+ every interface, so anyone on the same network could read the site being
36
+ written. To look at it from a phone on the same wifi, say so:
37
+ `npx docpensieve dev --host 0.0.0.0`.
38
+ - **Code blocks carry a background of their own.** A block in a language the
39
+ highlighter does not know used to sit on the bare page; it now looks like
40
+ every other block.
41
+ - **An error keeps its hint.** An error the tool raises while a page compiles
42
+ no longer comes out as a generic syntax complaint.
43
+
44
+ ## What is worth turning on
45
+
46
+ - [Snippet](../components/snippet/), to show a real file rather than a copy.
47
+ - [Hero](../components/hero/) and [Calendar](../components/calendar/).
48
+ - `copyCode`, a copy button on every code block — off by default, since it is
49
+ what makes a page load a script. See [the reference](../reference/configuration/).
50
+ - `snippetIcons`, if your project installs an icon collection.
36
51
 
37
52
  ## What to check afterwards
38
53
 
@@ -341,3 +341,6 @@ in none: a class of yours always wins, even to undo the default style.
341
341
  </Card>
342
342
 
343
343
  </ForTheme>
344
+
345
+ The commonest row is a row of cards; [Columns](./columns/) shows how to keep
346
+ them the same height, which a grid alone does not.
@@ -0,0 +1,230 @@
1
+ ---
2
+ title: Snippet
3
+ description: A code block that names a real file of your project, read when the page is generated, so that the example cannot go stale.
4
+ ---
5
+
6
+ # Snippet
7
+
8
+ An example written by hand is a copy. It stops being true the day the code
9
+ changes, and nothing reports it: the page goes on showing something that
10
+ worked once. `Snippet` names the file instead, and the file is read when the
11
+ page is generated — there is no copy left to drift.
12
+
13
+ The idea comes from **Christophe Avonture**, who built it for his own site.
14
+
15
+ ## Naming a file
16
+
17
+ <Snippet source="./snippet-example.js" />
18
+
19
+ ```mdx
20
+ <Snippet source="./snippet-example.js" />
21
+ ```
22
+
23
+ The file name labels the block, and the highlighting is the one every code
24
+ block gets. A path beginning with `./` or `../` is read **from the page**, as
25
+ a link is; any other path is read **from the root of your project**, so that
26
+ `src/index.js` means the same file from every page.
27
+
28
+ ## Part of a file
29
+
30
+ Two ways, and they do not age the same.
31
+
32
+ A **range of lines** is quick to write and means line 12 of the file as it is
33
+ today:
34
+
35
+ <Snippet source="./snippet-example.js" lines="9-10" title="The words it drops" />
36
+
37
+ ```mdx
38
+ <Snippet source="./snippet-example.js" lines="9-10" title="The words it drops" />
39
+ ```
40
+
41
+ Write `lines="9"` for one line, `"9-10"` for a range, `"9-"` from there to the
42
+ end, `"-10"` from the start. It says what the file holds today, and only
43
+ today.
44
+
45
+ A **region** is a marker left in the source file itself, in a comment. It
46
+ follows the code when the code moves, which is the whole point of naming a
47
+ file rather than copying it:
48
+
49
+ <Snippet source="./snippet-example.js" region="guard" title="The guard" />
50
+
51
+ ```mdx
52
+ <Snippet source="./snippet-example.js" region="guard" title="The guard" />
53
+ ```
54
+
55
+ In the file, the marker is an ordinary comment — the one editors already fold
56
+ on:
57
+
58
+ ```js
59
+ // #region guard
60
+ if (!name) {
61
+ throw new Error('greet needs a name');
62
+ }
63
+ // #endregion
64
+ ```
65
+
66
+ The indentation the kept lines share is removed, so a region marked inside a
67
+ function does not come out shifted.
68
+
69
+ ## Folding a long file
70
+
71
+ `collapsed` puts the block inside a `details`. No script: a long file can sit
72
+ in a page without burying what follows it.
73
+
74
+ <Snippet source="./snippet-example.js" collapsed title="The whole file" />
75
+
76
+ ```mdx
77
+ <Snippet source="./snippet-example.js" collapsed title="The whole file" />
78
+ ```
79
+
80
+ ## Code you write yourself
81
+
82
+ Without `source`, the component frames the code written in the page. A fenced
83
+ block inside the tag is the dependable way to write it: it carries anything,
84
+ braces and `<` included, and no formatter touches what is inside it.
85
+
86
+ <Snippet title="greet.py">
87
+
88
+ ```python
89
+ def greet(name, greetings={'en': 'Hello'}):
90
+ return f"{greetings['en']}, {name}!"
91
+ ```
92
+
93
+ </Snippet>
94
+
95
+ ````mdx
96
+ <Snippet title="greet.py">
97
+
98
+ ```python
99
+ def greet(name, greetings={'en': 'Hello'}):
100
+ return f"{greetings['en']}, {name}!"
101
+ ```
102
+
103
+ </Snippet>
104
+ ````
105
+
106
+ The extension of the title names the language; `lang` says it where the title
107
+ does not, as for a shell:
108
+
109
+ <Snippet title="Build the site" lang="bash">
110
+
111
+ ```bash
112
+ npm install
113
+ npx docpensieve build
114
+ ```
115
+
116
+ </Snippet>
117
+
118
+ ````mdx
119
+ <Snippet title="Build the site" lang="bash">
120
+
121
+ ```bash
122
+ npm install
123
+ npx docpensieve build
124
+ ```
125
+
126
+ </Snippet>
127
+ ````
128
+
129
+ ### Code typed straight into the tag
130
+
131
+ Code written bare inside the tag is read too, from the page as you typed it —
132
+ indentation kept, nothing taken for Markdown:
133
+
134
+ ```mdx
135
+ <Snippet title="who_are_you.py">name = input("What's your name? ")</Snippet>
136
+ ```
137
+
138
+ Two things it cannot carry, both of which happen before any component sees the
139
+ code. A page is MDX, so `{`, `}` and `<` are read as MDX and stop the build on
140
+ a syntax error. And a formatter run over your pages reflows what it takes for
141
+ prose: two lines of shell come back as one. Where either applies — which is
142
+ most real code — use the fenced form above.
143
+
144
+ ## The mark of the language
145
+
146
+ A block carries the colour of its language — a rule above it, and a short mark
147
+ beside the file name. The colour is set as a variable on the frame, so the
148
+ theme decides what it paints and a project can set another.
149
+
150
+ The same function, in the other file this page reads. Nothing was said about
151
+ the language: the extension carried it, and with it the mark and the colour.
152
+
153
+ <Snippet source="./snippet-example.py" region="guard" title="The same guard, in Python" />
154
+
155
+ ```mdx
156
+ <Snippet source="./snippet-example.py" region="guard" title="The same guard, in Python" />
157
+ ```
158
+
159
+ A block the page writes says its language in the fence, and is marked the same
160
+ way:
161
+
162
+ <Snippet title="deploy.yml">
163
+
164
+ ```yaml
165
+ steps:
166
+ - run: npx docpensieve build
167
+ - run: npx docpensieve check
168
+ ```
169
+
170
+ </Snippet>
171
+
172
+ <Snippet title="A first build" lang="bash">
173
+
174
+ ```bash
175
+ npx docpensieve init my-site
176
+ npx docpensieve dev
177
+ ```
178
+
179
+ </Snippet>
180
+
181
+ Where a project installs an icon collection and names it, the drawing replaces
182
+ the mark:
183
+
184
+ ```js
185
+ snippetIcons: 'simple-icons',
186
+ ```
187
+
188
+ It is never a reason to fail. The icon is the tool's idea, not something your
189
+ page asked for: a collection you do not have, or a language it does not draw,
190
+ brings back the mark. `icon="simple-icons:docker"` on a single block wins over
191
+ both.
192
+
193
+ ### A language nobody marked
194
+
195
+ Twenty-one languages have a mark and a colour. Any other is shown without
196
+ one — the frame, the name, and the code.
197
+
198
+ Highlighting is a separate matter, and a wider one: the highlighter knows
199
+ several hundred languages, and a block in one it does not know comes out as
200
+ plain text rather than stopping the build. You can write a snippet in anything
201
+ your project is written in.
202
+
203
+ ## What it refuses
204
+
205
+ Nothing fails quietly, because a snippet that resolved to nothing would leave
206
+ an empty frame — and an empty frame looks deliberate.
207
+
208
+ | Written | What happens |
209
+ | ----------------------------------- | --------------------------------------------------------------- |
210
+ | a file that is not there | The build stops, saying which path was looked for. |
211
+ | a path leading outside the project | Refused: a page is content, and content does not read the disk. |
212
+ | a region no marker opens, or closes | The build stops, naming the region and the file. |
213
+ | a range that reads backwards | The build stops. |
214
+
215
+ ## Properties
216
+
217
+ | Property | Type | Effect |
218
+ | ----------- | --------- | --------------------------------------------------------------------------- |
219
+ | `source` | `string` | File to read. From the page with `./` or `../`, from the project otherwise. |
220
+ | `title` | `string` | Label above the block. Defaults to the name of the file. |
221
+ | `lines` | `string` | `8`, `8-14`, `8-` or `-14`. |
222
+ | `region` | `string` | Name marked by `#region` and `#endregion` in the file. |
223
+ | `lang` | `string` | Language for the highlighting, when the extension says the wrong thing. |
224
+ | `collapsed` | `boolean` | Folds the block into a `details`. |
225
+ | `icon` | `string` | An icon for this block, instead of the mark of its language. |
226
+ | `className` | `string` | Classes added to the frame. |
227
+ | `style` | `object` | Inline styles on the frame. |
228
+
229
+ The file this page reads is installed along with it, so every example above
230
+ works in your project exactly as it does here.