You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Release history for Phosphor Docs. Each entry documents what changed and why.
4
+
5
+
## v0.2.0 — Security Hardening & Robustness
6
+
7
+
Released 2026-02-18.
8
+
9
+
A comprehensive security and stability audit uncovered 40+ issues across the parser, build system, config loader, CLI, and renderer. All have been fixed in this release.
10
+
11
+
### Security Fixes
12
+
13
+
:::warn Critical — update recommended
14
+
These fixes address real security vulnerabilities. If you accept user-contributed content (Markdown files, config values), update immediately.
15
+
:::
16
+
17
+
-**XSS in URLs**: All links, images, and hero buttons now escape URLs and reject `javascript:` URIs
18
+
-**Path traversal in build**: Custom favicon paths and page file paths are validated to stay within the project directory. Paths containing `../` that escape the project are rejected
19
+
-**SVG injection in favicon**: Theme color values injected into auto-generated favicons are now validated against safe patterns (`#hex` or `rgba()`)
20
+
21
+
### Parser Fixes
22
+
23
+
-**Code fences inside `:::` blocks**: A `:::` delimiter inside a code fence (` ``` `) is no longer treated as a component close. You can now safely show `:::` syntax in code examples within callouts, accordions, and other components
24
+
-**Unclosed `:::` blocks**: If a `:::tip` or other component block is never closed, the parser now warns to stderr and renders the content instead of silently consuming the rest of the file
25
+
-**Hyphenated component types**: The depth tracking regex now matches types like `decision-grid` (previously only matched `\w+` which excludes hyphens)
26
+
-**Heading regex performance**: Limited a repeating group in heading extraction to prevent potential catastrophic backtracking on malformed HTML
27
+
-**Table column padding**: Short table rows are now padded with empty cells to match the header column count, preventing misaligned tables
28
+
-**`data-` attribute support**: Component attribute parsing now accepts hyphenated names like `data-x="value"`
29
+
30
+
### Config & Build Robustness
31
+
32
+
-**Config type validation**: The config loader now validates that `pages` and `nav` are lists, `site` and `theme` are mappings. Wrong types produce clear error messages instead of downstream crashes
33
+
-**Template existence checks**: Missing `base.html` or `search.js` templates produce a clear error instead of an unhandled exception
34
+
-**Output directory permissions**: Permission denied errors when cleaning `_site/` are caught and reported clearly
35
+
-**Pages directory validation**: The build now checks that `pages/` exists before attempting to read files
36
+
-**Non-string theme values**: Integer or null values in the theme config are silently skipped instead of producing invalid CSS
37
+
38
+
### CLI Improvements
39
+
40
+
-**Port validation**: `phosphor serve -p` rejects port numbers outside the valid range (1-65535)
41
+
-**Port-in-use handling**: If the port is already in use, a friendly error message is shown instead of an unhandled exception
42
+
-**Build failure handling**: Build errors during `phosphor serve` and `phosphor build` are caught with a clean error message
43
+
-**Init guard**: `phosphor init` checks that the examples directory exists and handles directory creation failures
44
+
-**SVG MIME type**: The serve command now serves `.svg` files with the correct `image/svg+xml` content type
45
+
46
+
### Other
47
+
48
+
-**PyYAML version pin**: Changed from `>=6.0` to `>=6.0,<7.0` to prevent breaking changes from a future major version
49
+
-**install.sh**: Added `$HOME` environment check and chmod error handling
Copy file name to clipboardExpand all lines: pages/configuration.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -133,7 +133,7 @@ pages:
133
133
- Files not listed here are **not built** — this lets you keep drafts in `pages/` without publishing them
134
134
135
135
:::warn Every page must be listed
136
-
If a Markdown file exists in `pages/` but isn't listed in the `pages` array, it won't be included in the build. This is intentional — it gives you control over what gets published.
136
+
If a Markdown file exists in `pages/` but isn't listed in the `pages` array, it won't be included in the build. This is intentional — it gives you control over what gets published. The `pages` field must be a YAML list — strings or other types produce a clear error message.
137
137
:::
138
138
139
139
## Navigation
@@ -463,7 +463,7 @@ site:
463
463
favicon: "my-favicon.svg"
464
464
```
465
465
466
-
Place the favicon file in your project directory (next to `docs.yaml`). SVG format is recommended. Custom favicons are copied as-is and not themed.
466
+
Place the favicon file in your project directory (next to `docs.yaml`). SVG format is recommended. Custom favicons are copied as-is and not themed. The path must resolve within your project directory — paths that escape via `../` are rejected for security.
Orchestrator. Loads config, iterates over pages, calls parser/renderer/search, copies assets, writes output. The only module that does file I/O for the build.
Loads docs.yaml with PyYAML, merges with DEFAULTS dict. Validates types: `pages` and `nav` must be lists, `site` and `theme` must be mappings. Exits with clear error on invalid types.
The largest module. Two-pass Markdown-to-HTML converter. Pass 1: fenced component blocks (with code fence tracking). Pass 2: standard Markdown. Includes XSS protection for URLs and `javascript:` URI rejection. No external Markdown library.
- Group 1: block type — supports hyphenated names like `decision-grid`(required)
94
94
- Group 2: attribute string like `{title="..." usage="..."}` (optional)
95
95
- Group 3: inline text after the type (used for callout titles like `:::tip My Title`)
96
96
97
-
Nesting is tracked with a depth counter. Each `:::\w+` increments depth, each `:::` (bare) decrements. When depth hits 0, the block is closed.
97
+
Nesting is tracked with a depth counter. Each `:::type` increments depth, each `:::` (bare) decrements. When depth hits 0, the block is closed.
98
+
99
+
**Code fence awareness**: The fenced block scanner tracks code fence state. A `:::` delimiter inside a code fence (`` ``` ``) is ignored — it does not open or close a component block. This prevents content corruption when showing `:::` syntax examples inside code blocks.
100
+
101
+
**Unclosed blocks**: If a `:::type` block never finds its closing `:::`, the parser emits a warning to stderr and treats the remaining content as the block body rather than silently consuming it.
98
102
99
103
### How Code Fences Work
100
104
@@ -125,21 +129,24 @@ Cards and commands use `::child{attrs}` syntax (double colon, no triple). These
125
129
::flag\{([^}]*)\}\s*\n(.*?)(?=::flag|\Z)
126
130
```
127
131
128
-
Each child's attribute string is parsed by `_parse_attrs()` which extracts `key="value"` pairs.
132
+
Each child's attribute string is parsed by `_parse_attrs()` which extracts `key="value"` pairs. Attribute names support hyphens (e.g., `data-x="value"`) in addition to standard alphanumeric names.
129
133
130
134
### Inline Processing
131
135
132
136
The `_inline(text)` function handles inline Markdown within any line:
133
137
134
-
1.Images: `` -> `<img>`
135
-
2.Links with classes: `[text](url){.class}` -> `<a class="hero-btn class">`
136
-
3.Regular links: `[text](url)` -> `<a>`
137
-
4.Inline code: `` `code` ``-> `<code>`
138
+
1.**Code spans extracted first** — replaced with placeholders to protect from further processing
139
+
2.Images: `` -> `<img>` (URL escaped, alt text escaped)
140
+
3.Links with classes: `[text](url){.class}` -> `<a class="hero-btn class">`
141
+
4.Regular links: `[text](url)`-> `<a>`
138
142
5. Bold: `**text**` -> `<strong>`
139
143
6. Italic: `*text*` -> `<em>`
144
+
7.**Code spans restored** from placeholders
140
145
141
146
Order matters — images are processed before links to prevent `![` being matched as a regular link.
142
147
148
+
**URL security**: All URLs in links, images, and hero buttons are processed through `_escape_url()`, which HTML-escapes special characters (quotes, ampersands) and rejects `javascript:` URIs by replacing them with `#`.
149
+
143
150
### HTML Block Pass-Through
144
151
145
152
After pass 1, the text contains embedded HTML from component parsers. Pass 2 must not wrap these in `<p>` tags. The HTML block detection regex matches lines starting with known block-level HTML tags (both opening and closing):
@@ -173,15 +180,17 @@ The `build(project_dir)` function:
173
180
174
181
4.**Cleans output**: Deletes `_site/` entirely and recreates it. Every build is a clean build.
175
182
176
-
5.**Copies assets**: Copies `style.css`, `script.js`, and `favicon.svg` from `theme/` to `_site/assets/`. If a custom favicon is specified in config, it's copied over the default.
183
+
5.**Copies assets**: Copies `style.css`, `script.js`, and `favicon.svg` from `theme/` to `_site/assets/`. If a custom favicon is specified in config, it's copied only if the path resolves within the project directory (path traversal protection). Auto-generated favicons validate that theme colors match safe patterns (`#hex` or `rgba()`) before injecting them into SVG.
177
184
178
-
6.**Parses pages**: For each `.md` file in the `pages` config array, reads the file from `pages/`directory, calls `parser.parse_markdown()`, and stores the result.
185
+
6.**Validates and parses pages**: Checks that `pages/` directory exists. For each `.md` file in the `pages` config array, verifies the resolved path stays within `pages/`(path traversal protection), then reads the file and calls `parser.parse_markdown()`.
179
186
180
187
7.**Generates search**: Calls `search.build_search_index()` with all parsed page data. Injects the JSON index into the search.js template and writes it to `_site/assets/search.js`.
181
188
182
189
8.**Renders pages**: For each parsed page, calls `renderer.render_page()` which substitutes template variables and writes the HTML to `_site/`.
183
190
184
-
### Config Defaults
191
+
### Config Validation and Defaults
192
+
193
+
The config loader validates types before merging with defaults. `pages` and `nav` must be lists (or null/absent for defaults). `site` and `theme` must be mappings. Invalid types produce a clear error message and exit.
185
194
186
195
When `docs.yaml` is missing a field, these defaults apply:
Copy file name to clipboardExpand all lines: pages/reference.md
+27-10Lines changed: 27 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -25,13 +25,14 @@ Site built to /home/user/my-docs/_site/
25
25
26
26
The build process:
27
27
28
-
1. Loads and validates `docs.yaml`
29
-
2. Reads each `.md` file listed in the `pages` array
30
-
3. Parses Markdown into HTML (standard + extended components)
31
-
4. Generates the search index from all headings and content
32
-
5. Renders each page into the base HTML template
33
-
6. Copies theme assets (CSS, JS, favicon) to `_site/assets/`
34
-
7. Writes the final HTML files to `_site/`
28
+
1. Loads and validates `docs.yaml` (checks types: `pages` and `nav` must be lists, `site` and `theme` must be mappings)
29
+
2. Verifies that `pages/` directory exists and that all page paths stay within it (path traversal protection)
30
+
3. Reads each `.md` file listed in the `pages` array
31
+
4. Parses Markdown into HTML (standard + extended components)
32
+
5. Generates the search index from all headings and content
33
+
6. Renders each page into the base HTML template
34
+
7. Copies theme assets (CSS, JS, favicon) to `_site/assets/`
35
+
8. Writes the final HTML files to `_site/`
35
36
36
37
:::info Clean builds
37
38
Every build deletes and recreates the `_site/` directory from scratch. This ensures no stale files remain from previous builds. The `_site/` directory should be in your `.gitignore`.
@@ -83,11 +84,11 @@ Key behaviors:
83
84
Path to the project directory containing `docs.yaml`. Defaults to the current directory.
84
85
::
85
86
::flag{name="--port"short="-p"}
86
-
Port number for the local HTTP server. Defaults to 8000.
87
+
Port number for the local HTTP server (1-65535). Defaults to 8000.
87
88
::
88
89
:::
89
90
90
-
Builds the site and starts a local HTTP server for previewing. This is a convenience command that combines `phosphor build` with Python's built-in HTTP server.
91
+
Builds the site and starts a local HTTP server for previewing. This is a convenience command that combines `phosphor build` with Python's built-in HTTP server. Port numbers outside the valid range (1-65535) are rejected with a clear error. If the port is already in use, Phosphor prints a helpful message instead of crashing.
91
92
92
93
```terminal
93
94
$ phosphor serve
@@ -202,7 +203,7 @@ The base HTML template at `templates/base.html` defines the page shell:
202
203
| Dependency | Version | Purpose |
203
204
| --- | --- | --- |
204
205
| Python 3 | 3.8+ | Runtime |
205
-
| PyYAML | 6.0+|`docs.yaml` parsing |
206
+
| PyYAML | 6.0 - 6.x|`docs.yaml` parsing |
206
207
| Lucide Icons | CDN (latest) | Icon library (loaded at runtime from CDN) |
207
208
| Google Fonts | CDN | Chakra Petch, Nunito Sans, JetBrains Mono |
208
209
@@ -313,6 +314,22 @@ The search index might be empty. Verify:
313
314
If your docs project is on the Windows filesystem (`/mnt/c/...`), file operations are slow due to the WSL2 filesystem bridge. Move your project to the Linux filesystem (`~/my-docs/`) for faster builds.
314
315
:::
315
316
317
+
:::accordion{title="Error: port must be between 1 and 65535"}
318
+
The port number passed to `phosphor serve -p` is out of range. Use a port between 1 and 65535. Common choices: 3000, 5000, 8000, 8080.
319
+
:::
320
+
321
+
:::accordion{title="Error: port is already in use"}
322
+
Another process is using that port. Either stop the other process or use a different port: `phosphor serve -p 3001`
A file in the `pages` list references a path outside the `pages/` directory (e.g., `../../../etc/passwd`). Page paths must be simple filenames within the `pages/` directory. Remove any `../` or absolute paths from the `pages` list.
The `favicon` value in `docs.yaml` points outside the project directory. The favicon path must be relative and resolve within the project root. Remove any `../` sequences that escape the project.
331
+
:::
332
+
316
333
:::accordion{title="Lucide icons not rendering"}
317
334
Icons appear as empty squares or text. This means the Lucide CDN script isn't loading. Check:
318
335
1. You have internet connectivity (Lucide loads from `unpkg.com`)
0 commit comments