`. You can change this by adding the starting heading level after the import path:
+To exclude **all** members (methods, propeties, etc.) of a class, use:
+
+```md
+::: api jx.Catalog
+:show_members: false
+:::
+```
+
+### Changing the Starting Heading Level
-/// example | Custom Starting Heading Level
+By default, the name of the function or class is rendered with an `
`, and the names of attributes/methods with `
`. You can change this by adding the starting heading level after the import path:
```md
-::: jx.meta.extract_metadata level=4
+::: api jx.meta.extract_metadata
+:level: 4
+:::
```
-::: jx.meta.extract_metadata level=4
-///
+::: api jx.meta.extract_metadata
+:level: 4
+:::
+
+
+## Notes on Docstring Parsing
+
+The api module relies on the `docstring_parser` library to parse docstrings. It supports various docstring formats, but works best with Google-style docstrings.
+
+For optimal results:
+
+1. Start with a short, one-line description.
+2. Follow with a blank line and then a more detailed description.
+3. Use standard sections like "Arguments:" (or "Args:"), "Returns:", "Raises:", and "Examples:".
+4. Document all parameters, return values, and exceptions.
+
## Customizing the Output
-The extracted information is rendered using the `autodoc.jinja` view, recursively. There, you can see it receives a `ds` argument with these fields:
+The extracted information is rendered using the `api.jinja` view, recursively. There, you can see it receives a `ds` argument with these fields:
- `name`: The name of the documented element
- `symbol`: Type of the element (e.g., "class", "function", "method")
@@ -85,14 +122,3 @@ The extracted information is rendered using the `autodoc.jinja` view, recursivel
- `attrs`: List of ds objects for each attribute (for classes)
- `properties`: List of ds objects for each property (for classes)
- `methods`: List of ds objects for each method (for classes)
-
-## Notes on Docstring Parsing
-
-The autodoc module relies on the `docstring_parser` library to parse docstrings. It supports various docstring formats, but works best with Google-style docstrings.
-
-For optimal results:
-
-1. Start with a short, one-line description.
-2. Follow with a blank line and then a more detailed description.
-3. Use standard sections like "Arguments:" (or "Args:"), "Returns:", "Raises:", and "Examples:".
-4. Document all parameters, return values, and exceptions.
diff --git a/docs/content/languages.md b/docs/content/languages.md
index e44d817..ff0aa3c 100644
--- a/docs/content/languages.md
+++ b/docs/content/languages.md
@@ -133,10 +133,10 @@ build/
└── sitemap.xml
```
-/// note | One home page
+::: note | One home page
You can skip generating a home page for each language by using the option `skip_home=True`
in each language instance.
-///
+:::
### 4. Enable the language selector
diff --git a/docs/content/markdown/admonitions.md b/docs/content/markdown/admonitions.md
index d0b2559..aa03034 100644
--- a/docs/content/markdown/admonitions.md
+++ b/docs/content/markdown/admonitions.md
@@ -6,253 +6,153 @@ icon: icons/admonition.svg
Admonitions, also known as _call-outs_, are an excellent choice for including
side content without significantly interrupting the document flow:
-/// note | Some title
+::: note | Some title
Hi, this is an admonition box.
-///
+:::
## Syntax
Admonitions are created using the following syntax:
```md
-/// admonition | Some title
- type: classname
-
+::: type | Some optional title
Some content
-///
+:::
```
`type` will be used as the CSS class name and as the default title. It must be a
single word. For instance:
```md
-/// admonition
- type: note
-
+::: note
Some content.
More content.
-///
+:::
```
will render as:
-/// admonition
- type: note
-
+::: note
Some content.
More content.
-///
+:::
Optionally, you can use custom titles. For example:
```md
-/// admonition | Don't try this at home
- type: error
-
+::: error | Don't try this at home
This is an admonition box
-///
+:::
```
will render as:
-/// admonition | Don't try this at home
- type: error
-
+::: error | Don't try this at home
This is an admonition box
-///
-
-If you don't want a title, leave it blank:
-
-```md
-/// admonition |
- type: error
-
-This is an admonition box without a title. It's not very fancy, is it?
-///
-```
-
-results in:
-
-/// admonition |
- type: error
-
-This is an admonition box without a title. It's not very fancy, is it?
-///
+:::
## Supported types
-As a shortcut, there are a number of admonition blocks that can be used directly, like this:
+WriteADoc includes these default types: `note`, `tip`, `warning`, `error`, and `new`.
-```md
-/// note
-This is a note
-///
-```
-
-WriteADoc includes these default types: `note`, `tip`, `warning`, `error`, `new`, `example`, and `question`.
-
-/// note
+::: note
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et
euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
purus auctor massa, nec semper lorem quam in massa.
-///
+:::
-
-
-/// tip
+::: tip
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et
euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
purus auctor massa, nec semper lorem quam in massa.
-///
-
-
+:::
-/// warning
+::: warning
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et
euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
purus auctor massa, nec semper lorem quam in massa.
-///
-
-
+:::
-/// error
+::: error
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et
euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
purus auctor massa, nec semper lorem quam in massa.
-///
+:::
-
-
-/// new
+::: new
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et
euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
purus auctor massa, nec semper lorem quam in massa.
-///
-
-
-
-/// example
-In this type of admonition, the font size is slightly larger.
-///
-
-
-
-/// question
-Lorem ipsum dolor sit amet, consectetur
-adipiscing elit. Nulla et
-euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo
-purus auctor massa, nec semper lorem quam in massa.
-///
+:::
## Collapsible admonitions (details)
-If instead of `admonition` you use `details`, the admonition is rendered as a
+If you add an `open` option, the admonition is rendered as a
details/summary block with a small toggle on the right side:
```md
-/// details | Some summary
- type: warning
+::: note | Some summary
+:open: false
Some content
-///
+:::
```
will render as:
-/// details | Some summary
- type: warning
+::: note | Some summary
+:open: false
Some content
-///
+:::
-If you wish to specify a details block as open (not collapsed), simply use the `open` option.
+If you wish to specify a details block as open (not collapsed), simply use the `:open: true` option.
```md
-/// details | Some summary
- open: True
+::: note | Some summary
+:open: true
Some content
-///
+:::
```
will render as:
-/// details | Some summary
- open: True
+::: note | Some summary
+:open: true
Some content
-///
-
-Collapsible admonitions have the same predefined types as regular admonitions
-(`note`, `tip`, `warning`, `error`, `new`, `example`, and `question`), but unlike admonitions,
-details do not register any shortcut syntax by default.
-
-This feature uses the [`pymdownx.blocks.details`](https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/details/)
-extension, and can be configured in the markdown options.
+:::
## Inline admonitions
-If the screen is wide enough, you can have inline admonitions, aligned to the left or right, by wrapping the content
-in a `` tag and adding the "left" or "right" class to the admonition.
-Leave a blank line before and after the admonition.
-
-/// example | Admonition, aligned to the left (if your screen width >= 960px)
+If the screen is wide enough, you can have inline admonitions, by wrapping the content
+in a `:::: div columns` block (use *four* or more ":"):
-/// tip | Lorem Ipsum
- attrs: { class: left }
+:::::: div columns
+::: tip | Lorem Ipsum
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et euismod nulla.
-///
+:::
```md
-
-
-/// tip | Lorem Ipsum
- attrs: { class: left }
+::::: div columns
+::: tip | Lorem Ipsum
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nulla et euismod nulla.
-///
+:::
Some other content
-
-
-```
-
-///
-
-
-
-/// example | Admonition, aligned to the right (if your screen width >= 960px)
-
-/// tip | Lorem Ipsum
- attrs: { class: right }
-
-Lorem ipsum dolor sit amet, consectetur
-adipiscing elit. Nulla et euismod nulla.
-///
-
-```md
-
-
-/// tip | Lorem Ipsum
- attrs: { class: right }
-
-Lorem ipsum dolor sit amet, consectetur
-adipiscing elit. Nulla et euismod nulla.
-///
-
-Some other content
-
-
+:::::
```
-///
+::::::
diff --git a/docs/content/markdown/attributes.md b/docs/content/markdown/attributes.md
index 45d84c3..4038af8 100644
--- a/docs/content/markdown/attributes.md
+++ b/docs/content/markdown/attributes.md
@@ -37,68 +37,26 @@ The above results in the following code:
This is a paragraph.
```
-An exception is headers, as they are only ever allowed on one line.
-
-```md
-### A hash style header { .break }
-```
-
-The above results in the following code:
-
-```html
-
A hash style header
-```
-
## Inline
-To define attributes on inline elements, the attribute list should be placed immediately after the inline element with no whitespace.
+To define attributes on inline elements, the attribute list should be placed immediately after the inline element, with no spaces in between.
```md
-[link](http://example.com){: class="foo bar" title="Some title!" }
+[link](http://example.com){ class="foo bar" title="Some title!" }
```
The above results in the following output:
```html
-
```
-Attribute lists can be defined on table _cells_ (but not on tables themselves). To differentiate attributes for an inline element from attributes for the containing cell, the attribute list must be separated from the content by at least one space and placed at the end of the cell content. As table cells can only ever be on a single line, the attribute list must remain on the same line as the content of the cell.
-
-```md
-| set on td | set on em |
-|--------------|-------------|
-| *a* { .foo } | *b*{ .foo } |
-```
-
-The above example results in the following output:
-
-```html
-
-
-
-
set on td
-
set on em
-
-
-
-
-
a
-
b
-
-
-
-```
-
-Note that in the first column, the attribute list is preceded by a space; therefore, it is assigned to the table cell (`
` element). However, in the second column, the attribute list is not preceded by a space; therefore, it is assigned to the inline element (``) that immediately precedes it.
-
-Attribute lists may also be defined on table header cells (`
` elements) in the same manner.
-
## Limitations
-There are a few types of elements with which attribute lists do not work, most notably those HTML elements that are not represented in Markdown text, but only implied.
+There are a few types of elements with which attribute lists do not work, most notably those HTML elements that are not represented in Markdown text, but only implied. The attributes list feature **does not work** on:
-For example, the `ul` and `ol` elements do not exist in Markdown. They are only implied by the presence of list items (`li`).
-There is no way to use an attribute list to define attributes on implied elements, including but not limited to: `ul`, `ol`, `dl`, `blockquote`, `table`, `thead`, `tbody`, and `tr`.
+- Lists and list items: `ul`, `ol`, `dl`, and `li`
+- Block quotes,
+- Tables and table elements.
As a workaround, because Markdown is a subset of HTML, anything that cannot be expressed in Markdown can always be expressed directly with raw HTML.
diff --git a/docs/content/markdown/blocks.md b/docs/content/markdown/blocks.md
index 1a7cb64..9ae5543 100644
--- a/docs/content/markdown/blocks.md
+++ b/docs/content/markdown/blocks.md
@@ -7,8 +7,7 @@ icon: icons/blocks.svg
To create paragraphs, use a blank line to separate blocks of text.
-/// example |
-
+::: div example
```md
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at
faucibus quam, sit amet condimentum mi.
@@ -22,8 +21,7 @@ faucibus quam, sit amet condimentum mi.
Donec tellus turpis, posuere sit amet sem vitae, blandit efficitur erat.
Sed faucibus mollis enim ac molestie.
-
-///
+:::
## Headers
@@ -33,8 +31,7 @@ The number of number signs you use should correspond to the header level.
Always put a space between the number signs and the heading name, and use
blank lines before and after a header.
-/// example |
-
+::: div example
```md
# Header 1
@@ -60,8 +57,7 @@ blank lines before and after a header.
##### Header 5 {skip-toc=""}
###### Header 6 {skip-toc=""}
-
-///
+:::
## Line breaks
@@ -69,8 +65,7 @@ blank lines before and after a header.
To create a line break or new line, either add the HTML tag ` `, or end
a line with two or more spaces.
-/// example |
-
+::: div example
```md
First line with the HTML tag after.
And the next line.
@@ -84,32 +79,27 @@ And the next line.
First line with two spaces after.
And the next line.
-
-///
+:::
## Blockquote
To create a blockquote, add a `>` at the beginning of each line.
-/// example |
-
+::: div example
```md
> Dorothy followed her through many of the beautiful rooms
> in her castle.
```
-
> Dorothy followed her through many of the beautiful rooms
> in her castle.
-
-///
+:::
Blockquotes can contain multiple paragraphs. Add a `>` on the blank lines between paragraphs.
-/// example |
-
+::: div example
```md
> Dorothy followed her through many of the beautiful rooms
> in her castle.
@@ -123,8 +113,7 @@ Blockquotes can contain multiple paragraphs. Add a `>` on the blank lines betwee
>
> The Witch bade her clean the pots and kettles and sweep the
> floor and keep the fire fed with wood.
-
-///
+:::
## Horizontal Rules
@@ -132,8 +121,7 @@ Blockquotes can contain multiple paragraphs. Add a `>` on the blank lines betwee
To create a horizontal rule, use three or more asterisks (`***`),
dashes (`---`), or underscores (`___`) on a line by themselves.
-/// example |
-
+::: div example
```md
***
@@ -145,5 +133,4 @@ _________________
The rendered output of all three looks identical:
---
-
-///
\ No newline at end of file
+:::
diff --git a/docs/content/markdown/code.md b/docs/content/markdown/code.md
index 9c346dc..ef388ea 100644
--- a/docs/content/markdown/code.md
+++ b/docs/content/markdown/code.md
@@ -6,8 +6,7 @@ icon: icons/code.svg
To include a code block in your document, place three backticks (` ``` `) before and after the code block and, optionally, add a language name.
Leave one blank line before and after the block for easier reading.
-/// example | Code block
-
+::: div example
````md
```javascript
console.log("Hello world");
@@ -17,13 +16,9 @@ console.log("Hello world");
```javascript
console.log("Hello world");
```
+:::
-///
-
-
-
-/// details | Tip: Display triple backticks in a code block
- {type: tip}
+::: tip | Tip: Display triple backticks in a code block
You can always add _more_ backticks! For example, to display triple backticks in a code block, wrap them inside quadruple backticks.
`````md
@@ -41,16 +36,14 @@ renders as:
Look! You can see my backticks.
```
````
-
-///
+:::
## Syntax highlighting
To make your code block clearer, specify the language right after the first three backticks to enable syntax highlighting.
-/// example | Syntax highlighting
-
+::: div example
````md
```python
print("Hello, world!")
@@ -66,8 +59,7 @@ print("Hello, world!")
for i in range(10):
print(i)
```
-
-///
+:::
Internally, this uses the **Pygments** library. Check the [long list of available languages](https://pygments.org/languages/).
This only adds HTML classes; the styles and colors themselves are controlled by CSS.
@@ -78,8 +70,7 @@ This only adds HTML classes; the styles and colors themselves are controlled by
To show line numbers in your code block, specify the starting line number with the option `linenums="1"` after the opening tokens (and language, if present).
The number _must_ be quoted, and it is the number of the first line (it must be greater than 0).
-/// example | Line Numbers
-
+::: div example
````md
```python {linenums="1"}
import foo.bar
@@ -97,13 +88,11 @@ import foo.bar
a = "lorem"
b = "ipsum"
```
-
-///
+:::
If you want to start with a different line number, simply specify a number other than `1`.
-/// example | Line Numbers
-
+::: div example
````md
```python {linenums="42"}
def foobar():
@@ -123,15 +112,13 @@ def foobar():
foobar()
```
-
-///
+:::
Pygments also has a few additional options regarding line numbers. One is "line step," which, if set to a number larger than 1, will print only every n^th^ line number.
-/// example | N^th^ line
-
+::: div example
````md
-``` {linenums="1 2"}
+```python {linenums="1 2"}
"""Some file."""
import foo.bar
import boo.baz
@@ -141,22 +128,20 @@ import foo.bar.baz
renders as:
-``` {linenums="1 2"}
+```python {linenums="1 2"}
"""Some file."""
import foo.bar
import boo.baz
import foo.bar.baz
```
-
-///
+:::
## Highlighting lines
You can specify certain lines for highlighting by using the `hl_lines` setting directly after the opening tokens (and language, if present), with the targeted line numbers separated by spaces.
-/// example | Highlight Lines
-
+::: div example
````md
```python {hl_lines="1 3"}
"""Some file."""
@@ -174,13 +159,11 @@ import foo.bar
import boo.baz
import foo.bar.baz
```
-
-///
+:::
Line numbers are always referenced starting at 1, regardless of what the line number is labeled as when showing line numbers.
-/// example | Highlight Lines with Line Numbers
-
+::: div example
````md
```python {linenums="42" hl_lines="2"}
def foobar():
@@ -200,13 +183,11 @@ def foobar():
foobar()
```
-
-///
+:::
If you'd like to highlight a range of lines, you can use the notation x-y, where x is the starting line and y is the ending line. You can specify multiple ranges and even mix them with individual lines.
-/// example | Highlight Lines with Line Numbers
-
+::: div example
````md
```python {hl_lines="1-2 5 7-8"}
import foo
@@ -234,16 +215,14 @@ class Foo:
self.bar = None
self.baz = None
```
-
-///
+:::
## Title Headers
A header with a title can be applied to a code block using the title option. Typically, you use it to show a filename, but it can be anything.
-/// example | Code block with a header
-
+::: div example
````md
```python {title="cool_file.py"}
import foo
@@ -271,5 +250,4 @@ class Foo:
self.bar = None
self.baz = None
```
-
-///
+:::
diff --git a/docs/content/markdown/formatting.md b/docs/content/markdown/formatting.md
index d65d0b2..c76df4e 100644
--- a/docs/content/markdown/formatting.md
+++ b/docs/content/markdown/formatting.md
@@ -5,103 +5,69 @@ icon: icons/format.svg
## Emphasis
-To italicize text, add one asterisk or one underscore before and after a word or phrase.
-To bold text, add two asterisks or two underscores before and after a word or phrase.
+To italicize text, add one underscore (or one asteeisk) before and after a word or phrase.
+To bold text, add two asterisks (or two underscores) before and after a word or phrase.
There cannot be spaces following the opening token(s) or preceding the closing token(s).
-/// example |
-
+::: div example
```md
-This **is bold**, __and also this__
+This **is bold** and __also this__
-This *is italicized*, _and also this_
+This _is italicized_ and *also this*
This * won't emphasize *
```
-This **is bold**, __and also this__
+This **is bold** and __also this__
-This *is italicized*, _and also this_
+This _is italicized_ and *also this*
This * won't emphasize *
+:::
-///
-
-### Bold and Italic
-
-When mixing bold and italic, WriteADoc will try to prioritize the most sensible option when nesting bold (**) within italic (*) and vice versa.
-
-/// example |
-
-```md
-***I'm italic and bold* I am just bold.**
-
-***I'm bold and italic!** I am just italic.*
-
-*I'm italic. **I'm bold and italic.** I'm also just italic.*
-```
-
-***I'm italic and bold* I am just bold.**
-
-***I'm bold and italic!** I am just italic.*
-
-*I'm italic. **I'm bold and italic.** I'm also just italic.*
-
-///
-
-
-/// details | Complex examples
- type: example
+When mixing bold and italic, stick with asterisks (**) for bold and underscores (_) for italics.
+::: div example
```md
-__This will all be bold __because of the placement of the center underscores.__
-
-__This will all be bold __ because of the placement of the center underscores.__
+**_I'm italic and bold_ I am just bold.**
-__This will NOT all be bold__ because of the placement of the center underscores.__
+_**I'm bold and italic!** I am just italic._
-__This will all be bold_ because the token is less than that of the surrounding.__
+_I'm italic. **I'm bold and italic.** I'm also just italic._
```
-__This will all be bold __because of the placement of the center underscores.__
+**_I'm italic and bold_ I am just bold.**
-__This will all be bold __ because of the placement of the center underscores.__
+_**I'm bold and italic!** I am just italic._
-__This will NOT all be bold__ because of the placement of the center underscores.__
-
-__This will all be bold_ because the token is less than that of the surrounding.__
-
-///
+_I'm italic. **I'm bold and italic.** I'm also just italic._
+:::
## Code
To denote a word or phrase as code, enclose it in backticks (`).
-/// example |
-
+::: div example
```md
To run the command, press `ENTER`.
```
To run the command, press `ENTER`.
-
-///
+:::
### Escaping Backticks
If the word or phrase you want to denote as code includes one or more backticks, you can escape it by enclosing the word or phrase in double backticks (``).
-/// example |
-
+::: div example
```md
``Use `code` in your Markdown file.``
```
``Use `code` in your Markdown file.``
-
-///
+:::
## Sub- and superscripts
@@ -110,8 +76,7 @@ With this simple syntax, text can be subscripted and superscripted, which is mor
To make a subscript, surround the content with a single `~`. To make a superscript, surround the content with `^`. In both cases, if you need to include spaces, you must escape them.
-/// example |
-
+::: div example
```md
CH~3~CH~2~OH
@@ -129,8 +94,7 @@ text~a\ subscript~
a^2^ + 2ab + b^2^
text^a\ superscript^
-
-///
+:::
## Highlighting changes
@@ -139,8 +103,7 @@ Text changes can be highlighted with a simple syntax, which is more convenient t
To highlight text, surround it with double `=`. To highlight an insertion, use double `^`, and to highlight a deletion, use double `~`.
-/// example |
-
+::: div example
```md
- ==This was marked (highlight)==
- ^^This was inserted (underline)^^
@@ -150,26 +113,4 @@ To highlight text, surround it with double `=`. To highlight an insertion, use d
- ==This was marked (highlight)==
- ^^This was inserted (underline)^^
- ~~This was deleted (strikethrough)~~
-
-///
-
-
-## Symbols
-
-Although Markdown doesn't have native support for including special symbols, WriteADoc makes it easy to create *some* special characters, such as trademarks, arrows, fractions, etc.
-
-| Markdown | Result
-| ---------------- | -------------
-| `(tm)` | (tm)
-| `(c)` | (c)
-| `(r)` | (r)
-| `c/o` | c/o
-| `+/-` | +/-
-| `-->` | -->
-| `<--` | <--
-| `<-->` | <-->
-| `=/=` | =/=
-| `1/2, 1/4, etc.` | 1/2, 1/4, etc.
-| `1st 2nd etc.` | 1st 2nd etc.
-
-For anything else, you can use HTML code or Unicode characters: 👈 😍
+:::
diff --git a/docs/content/markdown/images.md b/docs/content/markdown/images.md
index 7971966..b74ce53 100644
--- a/docs/content/markdown/images.md
+++ b/docs/content/markdown/images.md
@@ -5,22 +5,19 @@ icon: icons/image.svg
To add an image, use an exclamation mark (`!`), followed by the alt text in brackets, and the path or URL to the image asset in parentheses. You can optionally add a title in quotation marks after the path or URL.
-/// example | An image
-
+::: div example
```md

```

-
-///
+:::
## Light and dark mode
You can show different images for light and dark color schemes by using the `only-light` or `only-dark` classes.
-/// example | Image, different for light and dark mode
-
+::: div example
```md
{ .only-light }
{ .only-dark }
@@ -28,115 +25,84 @@ You can show different images for light and dark color schemes by using the `onl
{ .only-light }
{ .only-dark }
-
-///
+:::
If you don't want to have two versions of the image, you can instead use the `invert` class, which inverts
the colors of the image **only in dark mode**. It doesn't look great for some images, but it's good enough most of the time.
-/// example | Image, inverted colors in dark mode
-
+::: div example
```md
{ .invert }
```
{ .invert }
-
-///
+:::
## Image alignment
If the screen is wide enough, you can align images to the left or right by wrapping the content
-in a `` tag and adding the "left" or "right" class to the image.
-
-/// example | Image, aligned to the left (if your screen width >= 960px)
+in a `::: div columns` block.
+:::: div example
{ .invert .left }
```md
-
-
+::: div columns
{ .left }
Aliqua id elit sint ullamco cillum consequat.
-Proident ad elit laboris consectetur duis sint proident voluptate
-incididunt nulla excepteur culpa tempor.
-
-```
-
-///
-
-/// example | Image, aligned to the right (if your screen width >= 960px)
-
-{ .invert .right }
-
-```md
-
-
-{ .right }
-
-Aliqua id elit sint ullamco cillum consequat.
-
-Proident ad elit laboris consectetur duis sint proident voluptate
-incididunt nulla excepteur culpa tempor.
-
+Proident ad elit laboris consectetur duis sint
+proident voluptate incididunt nulla excepteur
+culpa tempor.
+:::
```
+::::
-///
-
-To center an image, just add the class
-
-/// example | Image, centered
+To center an image, just add the `center` class.
+::: div example
```md
{ .center }
-
```
{ .invert .center }
-///
+:::
-## Image captions
+## Figures
-Unfortunately, the Markdown syntax doesn't provide native support for image captions, but you can use the [Markdown in HTML](/docs/md/markdown/html/#markdown-in-html) feature with literal `figure` and `figcaption` tags:
+The Markdown syntax doesn't provide native support for image captions, but you can use a `::: figure` block:
-/// example | Image, centered
+:::: div example
-```md
-
+```markdown
+::: figure | Image caption

-Image caption
-
+:::
```
-
-{ .invert }
-Image caption
-
-
-///
+::: figure | Image caption
+
+:::
+::::
## Image links
To add a link to an image, enclose the Markdown for the image in brackets, and then add the link in parentheses.
-/// example | A linked image
-
+:::: div example
```md
[](https://example.com/)
```
[{ .invert }](https://example.com/)
-
-///
+::::
## Forcing image size
You can force an image to have a specific width and/or height by adding attributes. Set just one of them to resize the image while preserving its aspect ratio, or set both to distort it.
-/// example | Images with width/height
-
+::: div example
```md
{ width=100 }
@@ -146,19 +112,16 @@ You can force an image to have a specific width and/or height by adding attribut
{ .invert width=100 }
{ .invert width=300 height=50 }
-
-///
+:::
## Lazy-loading images
Modern browsers provide [native support for lazy-loading images](https://caniuse.com/loading-lazy-attr) through the `loading=lazy` attribute, which falls back to normal eager-loading in browsers without support.
-/// example | Image, centered
-
+::: div example
```md
{ loading=lazy }
```
{ loading=lazy width=600 }
-
-///
+:::
diff --git a/docs/content/markdown/links.md b/docs/content/markdown/links.md
index ca1f935..a396a53 100644
--- a/docs/content/markdown/links.md
+++ b/docs/content/markdown/links.md
@@ -5,35 +5,30 @@ icon: icons/link.svg
To create a link, enclose the link text in brackets (e.g., [Duck Duck Go]), and then follow it immediately with the URL in parentheses (e.g., (https://duckduckgo.com)).
-/// example | Links
-
+::: div example
```md
My favorite search engine is [Duck Duck Go](https://duckduckgo.com).
```
My favorite search engine is [Duck Duck Go](https://duckduckgo.com).
-
-///
+:::
You can optionally add a title to a link. This will appear as a tooltip when the user hovers over the link. To add a title, enclose it in quotation marks after the URL.
-/// example | Links with title
-
+::: div example
```md
My favorite search engine is [Duck Duck Go](https://duckduckgo.com "The best for privacy").
```
My favorite search engine is [Duck Duck Go](https://duckduckgo.com "The best for privacy").
-
-///
+:::
## Quick links
To quickly turn a URL or email address into a link, enclose it in angle brackets.
-/// example | Quick links
-
+::: div example
```md
@@ -43,15 +38,13 @@ To quickly turn a URL or email address into a link, enclose it in angle brackets
-
-///
+:::
## Formatting Links
To emphasize links, add asterisks before and after the brackets and parentheses. To denote links as code, add backticks inside the brackets.
-/// example | Links with format
-
+::: div example
```md
The **[EFF website](https://eff.org)**.
@@ -65,20 +58,17 @@ The **[EFF website](https://eff.org)**.
This is the *[Markdown Guide](https://www.markdownguide.org)*.
See the section on [`code`](#code).
-
-///
+:::
## Attributes
-You can add extra attributes to a link, like `target="blank"`, using the [attribute lists](/attributes/) syntax.
-
-/// example | Link with extra attributes
+You can add extra attributes to a link, like `target="blank"`, using the [attribute lists](/docs/markdown/attributes/) syntax.
+::: div example
```md
[Opens in a new tab](https://www.python.org/){ target="blank" }
```
[Opens in a new tab](https://www.python.org/){ target="blank" }
-
-///
\ No newline at end of file
+:::
\ No newline at end of file
diff --git a/docs/content/markdown/lists/ordered.md b/docs/content/markdown/lists/ordered.md
index 2433cbf..e69c17f 100644
--- a/docs/content/markdown/lists/ordered.md
+++ b/docs/content/markdown/lists/ordered.md
@@ -3,126 +3,68 @@ title: Ordered Lists
icon: icons/list-ol.svg
---
-To create an ordered list, add line items with numbers followed by periods. You can also use `#` instead of numbers.
-WriteADoc extends the list handling formats to support parenthesis-style lists along with additional ordered formats.
-
-/// example | Numerical
+To create an ordered list, add line items with numbers followed by periods or parenthesis.
+::: div example
```md
1. First item
2. Second item
-#. Third item
+3. Third item
```
1. First item
2. Second item
-#. Third item
-
-///
-
-
-
-/// example | Alphabetical
-
-For uppercase, use two spaces after the dot (or a parenthesis instead).
-
-```md
-a. First item
-b. Second item
-c. Third item
-
-A. First item
-B. Second item
-C. Third item
-```
-
-a. First item
-b. Second item
-c. Third item
+3. Third item
+:::
-A. First item
-B. Second item
-C. Third item
-
-///
-
-
-
-/// example | Roman numerals
-
-For uppercase, use two spaces after the dot (or a parenthesis instead).
+You can also start the list with a different number
+::: div example
```md
-i. First item
-ii. Second item
-iii. Third item
-
-I. First item
-II. Second item
-III. Third item
-
+6. apples
+7. oranges
+8. pears
```
-i. First item
-ii. Second item
-iii. Third item
-
-I. First item
-II. Second item
-III. Third item
-
-///
-
+6. apples
+7. oranges
+8. pears
+:::
## Nested lists
Indent with two spaces to create a nested list.
-/// example |
-
+::: div example
```md
1) Item 1
2) Item 2
- i. Item 1
- ii. Item 2
- a. Item a
- b. Item b
- #. Item 1
- #. Item 2
+ 1. Item 1
+ 2. Item 2
+ 1. Item a
+ 2. Item b
+ 1. Item 1
+ 2. Item 2
```
1) Item 1
2) Item 2
- i. Item 1
- ii. Item 2
- a. Item a
- b. Item b
- #. Item 1
- #. Item 2
+ 1. Item 1
+ 2. Item 2
+ 1. Item a
+ 2. Item b
+ 1. Item 1
+ 2. Item 2
+:::
-///
+## Adjacent lists
-## Features
+A new list will be created if the list type changes. This occurs with:
-- Supports ordered lists with either a trailing dot or a single right parenthesis: `1.` or `1)`.
-- Supports ordered lists with Roman numeral formats, both lowercase and uppercase. Uppercase is treated as a different list type than lowercase.
-- Supports ordered lists with alphabetical format, both lowercase and uppercase. Uppercase is treated as a different list type than lowercase.
-- Supports a generic ordered list marker via `#.` or `#)`. These can be used in place of numerals and will inherit the type of the current list as long as they use the same convention (`.` or `)`). If used to start a list, decimal format will be assumed.
-- Using a different list type will start a new list. Trailing dot vs. parenthesis are treated as separate types.
-- Ordered lists are sensitive to the starting value and can restart a list or create a new list using the first value in the list.
-
-
-## Rules
-
-### 1. A new list will be created if the list type changes
-
-This occurs with:
-
-a. A switch between unordered and ordered.
-
-/// example |
+**a. A switch between unordered and ordered.**
+::: div example
```md
- Item 1
- Item 2
@@ -134,13 +76,11 @@ a. A switch between unordered and ordered.
- Item 2
1. Item 1
2. Item 2
+:::
-///
-
-b. A change from using a trailing dot to a single right parenthesis.
-
-/// example |
+**b. A change from using a trailing dot to a single right parenthesis.**
+::: div example
```md
1. Item 1
1. Item 2
@@ -152,109 +92,4 @@ b. A change from using a trailing dot to a single right parenthesis.
1. Item 2
1) Item 1
2) Item 2
-
-///
-
-c. A change between using uppercase and lowercase.
-
-/// example |
-
-```md
-a. Item a
-b. Item a
-A. Item A
-B. Item B
-```
-
-a. Item a
-b. Item a
-A. Item A
-B. Item B
-
-///
-
-d. A change in ordered type: numerical, Roman numeral, alphabetical, or generic.
-
-/// example |
-
-```md
-#. Item 1
-#. Item 2
-a. Item a
-b. Item b
-1. Item 1
-2. Item 2
-```
-
-#. Item 1
-#. Item 2
-a. Item a
-b. Item b
-1. Item 1
-2. Item 2
-
-///
-
-### 2. Generic list items inherit the type from the current list and, if starting a new list, will assume the decimal type
-
-List items following a generic list will not cause a new list as long as the list item is consistent with the current list type.
-
-/// example |
-
-```md
-i. item i
-#. item ii
-#. item iii
-iv. item iv
-```
-
-i. item i
-#. item ii
-#. item iii
-iv. item iv
-
-///
-
-### 3. If using uppercase list markers, a list marker consisting of a single uppercase letter followed by a dot will require two spaces after the marker instead of the usual one, to avoid false positive matches with names that start with an initial
-
-/// example |
-
-```md
-B. Russell was an English philosopher.
-
-A. This is a list.
-```
-
-B. Russell was an English philosopher.
-
-A. This is a list.
-
-///
-
-### 4. If a single letter is used to start a list, it is assumed to be an alphabetical list unless the first letter is `i` or `I`
-
-/// example |
-
-```md
-h. Item h
-i. Item i
-j. Item j
-
----
-
-i. Item 1
-ii. Item 2
-iii. Item 3
-```
-
-h. Item h
-i. Item i
-j. Item j
-
----
-
-i. Item 1
-ii. Item 2
-iii. Item 3
-
-///
+:::
diff --git a/docs/content/markdown/lists/tasks.md b/docs/content/markdown/lists/tasks.md
index 2a441b9..09dd260 100644
--- a/docs/content/markdown/lists/tasks.md
+++ b/docs/content/markdown/lists/tasks.md
@@ -6,12 +6,11 @@ icon: icons/list-check.svg
WriteADoc supports _Github Flavored Markdown_ (GFM) style task lists. They follow the same syntax as GFM.
Simply start each list item with a square bracket pair containing either a space (an unchecked item) or a `x` (a checked item).
-/// example |
-
+::: div example
```md
- [X] item 1
* [X] item A
- * [ ] item B
+ * [ ] item B
more text
+ [x] item a
+ [ ] item b
@@ -23,7 +22,7 @@ Simply start each list item with a square bracket pair containing either a space
- [X] item 1
* [X] item A
- * [ ] item B
+ * [ ] item B
more text
+ [x] item a
+ [ ] item b
@@ -31,5 +30,4 @@ Simply start each list item with a square bracket pair containing either a space
* [X] item C
- [ ] item 2
- [ ] item 3
-
-///
+:::
\ No newline at end of file
diff --git a/docs/content/markdown/lists/unordered.md b/docs/content/markdown/lists/unordered.md
index 61b9fa2..34a4c43 100644
--- a/docs/content/markdown/lists/unordered.md
+++ b/docs/content/markdown/lists/unordered.md
@@ -5,8 +5,7 @@ icon: icons/list-ul.svg
To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items.
-/// example |
-
+::: div example
```md
- First item
- Second item
@@ -18,17 +17,15 @@ To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in
- Second item
- Third item
- Fourth item
-
-///
+:::
Indent one or more items to create a nested list.
-/// example |
-
+::: div example
```md
- item 1
* item A
- * item B
+ * item B
more text
+ item a
+ item b
@@ -40,7 +37,7 @@ Indent one or more items to create a nested list.
- item 1
* item A
- * item B
+ * item B
more text
+ item a
+ item b
@@ -48,15 +45,13 @@ Indent one or more items to create a nested list.
* item C
- item 2
- item 3
-
-///
+:::
## Starting Unordered List Items With Numbers
If you need to start an unordered list item with a number followed by a period, you can use a backslash (\) to escape the period.
-/// example |
-
+::: div example
```md
- 1979\. A great year!
- I think 1983 was second best.
@@ -64,15 +59,13 @@ If you need to start an unordered list item with a number followed by a period,
- 1979\. A great year!
- I think 1983 was second best.
-
-///
+:::
## List items with paragraphs
To add another element in a list while preserving the continuity of the list, indent the element **four spaces** or **one tab**, as shown in the following examples.
-/// example | Paragraphs
-
+::: div example
```md
* This is the first list item.
* Here's the second list item.
@@ -88,13 +81,9 @@ To add another element in a list while preserving the continuity of the list, in
I need to add another paragraph below the second list item.
* And here's the third list item.
+:::
-///
-
-
-
-/// example | Blockquotes
-
+::: div example
```md
* This is the first list item.
* Here's the second list item.
@@ -110,13 +99,9 @@ To add another element in a list while preserving the continuity of the list, in
> A blockquote would look great below the second list item.
* And here's the third list item.
+:::
-///
-
-
-
-/// example | Images
-
+::: div example
```md
1. Open the file containing the Linux mascot.
2. Marvel at its beauty.
@@ -132,6 +117,5 @@ To add another element in a list while preserving the continuity of the list, in

3. Close the file.
-
-///
+:::
diff --git a/docs/content/markdown/tables.md b/docs/content/markdown/tables.md
index 8bc11e8..76f3c9b 100644
--- a/docs/content/markdown/tables.md
+++ b/docs/content/markdown/tables.md
@@ -5,40 +5,35 @@ icon: icons/table.svg
To add a table, use three or more hyphens (`---`) to create each column’s header, and use pipes (`|`) to separate each column. You can also add a pipe at either end of the row, but it is not necessary.
-/// example | Table
-
+::: div example
```md
-| Method | Description |
-| ----------- | --------------- |
-| `GET` | Fetch resource |
-| `PUT` | Update resource |
-| `DELETE` | Delete resource |
+Method | Description
+----------- | ---------------
+`GET` | Fetch resource
+`PUT` | Update resource
+`DELETE` | Delete resource
```
-| Method | Description |
-| ----------- | --------------- |
-| `GET` | Fetch resource |
-| `PUT` | Update resource |
-| `DELETE` | Delete resource |
-
-///
+Method | Description
+----------- | ---------------
+`GET` | Fetch resource
+`PUT` | Update resource
+`DELETE` | Delete resource
+:::
You don't have to make each cell the same width, but it looks clearer if you do.
-/// tip
-
+::: tip
Creating tables with hyphens and pipes can be tedious. To speed up the process, try using the [Markdown Tables Generator](https://www.tablesgenerator.com/markdown_tables). Build a table using the graphical interface, and then copy the generated Markdown-formatted text into your file.
-
-///
+:::
## Alignment
You can align text in the columns to the left, right, or center by adding a colon (`:`) to the left, right, or on both sides of the hyphens within the header row.
-/// example | Table with aligned columns
-
+::: div example
```md
| Syntax | Description | Test Text |
@@ -52,5 +47,4 @@ You can align text in the columns to the left, right, or center by adding a colo
| :--- | :----: | ---: |
| Header | Title | Here's this |
| Paragraph | Text | And more |
-
-///
+:::
diff --git a/docs/content/markdown/tabs.md b/docs/content/markdown/tabs.md
index ce19651..f50f466 100644
--- a/docs/content/markdown/tabs.md
+++ b/docs/content/markdown/tabs.md
@@ -5,84 +5,72 @@ icon: icons/tab.svg
Some parts of your documentation might become clear by organzing them in tabs, such as language-specific code snippets (e.g., tabs for Python, JavaScript, etc.) or an example that uses several files (e.g. a tabs for HTML, CSS, and JavaScript of a component)
-A tab is defined using the `///` syntax and the name `tab`. Tabs should also specify the tab title in the
-header. Consecutive tabs will automatically be grouped.
-
-/// example | Tabs
+A tab is defined using the `:::` syntax and the name `tab`. Tabs should also specify the tab title in the header. Consecutive tabs will automatically be grouped.
+:::: div example
```md
-/// tab | Tab 1 title
+::: tab | Tab 1 title
Tab 1 content
-///
+:::
-/// tab | Tab 2 title
+::: tab | Tab 2 title
Tab 2 content
-///
+:::
```
-//// tab | Tab 1 title
+::: tab | Tab 1 title
Tab 1 content
-////
+:::
-//// tab | Tab 2 title
+::: tab | Tab 2 title
Tab 2 content
-////
-///
+:::
+::::
If you want to have two tab containers right after each other, you specify a hard break that will force the specified tab to start a brand new tab container.
-/// example | New Tab Group
-
+:::: div example
```md
-/// tab | Tab A title
+::: tab | Tab A title
Tab A content
-///
+:::
-/// tab | Tab B title
+::: tab | Tab B title
Tab B content
-///
+:::
-/// tab | Tab C Title
- new: true
+::: tab | Tab C Title
+:new: true
Will be part of a separate, new tab group.
-///
+:::
```
-//// tab | Tab A title
+::: tab | Tab A title
Tab A content
-////
+:::
-//// tab | Tab B title
+::: tab | Tab B title
Tab B content
-////
+:::
+
+::: tab | Tab C title
+:new: true
-//// tab | Tab C title
- new: true
Will be part of a separate, new tab group.
-////
-///
+:::
+::::
If desired, you can specify a tab to be selected by default with the `select` option.
```md
-/// tab | Tab 1 title
+::: tab | Tab 1 title
Tab 1 content
-///
+:::
-/// tab | Tab 2 title
- select: True
+::: tab | Tab 2 title
+:select: True
Tab 2 should be selected by default.
-///
-```
-
-As with other blocks, you can always add new classes, and id or other attributes via the `attrs` option.
-
-```md
-/// tab | Some title
- attrs: {class: class-name: id: id-name}
-
-Some content
-///
+:::
```
diff --git a/docs/content/quickstart/config.md b/docs/content/quickstart/config.md
index 926d7a2..d620f9b 100644
--- a/docs/content/quickstart/config.md
+++ b/docs/content/quickstart/config.md
@@ -43,17 +43,13 @@ docs = Docs(
)
```
-///warning
+::: warning
Include every page **except** your `index.md` file.
-///
+:::
A page is specified as the path of a Markdown file, relative to the `content` folder.
-
+{ .only-light }
+{ .only-dark }
+:::
The title shown will be extracted from the page metadata `title`.
@@ -75,11 +73,7 @@ If you want to display them inside a "folder," put them inside a section.
You can group pages into sections, which can also contain subsections.
-
+{ .only-light }
+{ .only-dark }
+:::
The `icon` is optional. If included, it should be a path, relative to the assets folder, of an image or SVG file.
@@ -112,11 +108,7 @@ file, relative to the `content` folder.
You can still define a `title`, but it is optional, because it will be extracted from the page metadata.
If there is an `icon` in the page metadata—a path, relative to the assets folder, of an image or SVG file—it will also be shown.
-
+{ .only-light }
+{ .only-dark }
+:::
Clicking on the section title will show its page.
diff --git a/docs/content/quickstart/setup.md b/docs/content/quickstart/setup.md
index 0114afe..ec8349e 100644
--- a/docs/content/quickstart/setup.md
+++ b/docs/content/quickstart/setup.md
@@ -8,40 +8,32 @@ title: Installation
Use your package manager
-/// tab | Using "**uv**"
-
+::: tab | Using "**uv**"
```bash
uv add writeadoc --group docs
```
+:::
-///
-
-/// tab | Using "**Poetry**"
-
+::: tab | Using "**Poetry**"
```bash
poetry add writeadoc --group docs
```
-
-///
+:::
### Or install it by itself
-/// tab | Using "**uv**"
-
+::: tab | Using "**uv**"
```bash
uv pip install writeadoc
```
+:::
-///
-
-/// tab | Using "**pip**"
-
+::: tab | Using "**pip**"
```bash
pip install writeadoc
```
-
-///
+:::
## Creating a new project
@@ -80,19 +72,18 @@ Open [http://127.0.0.1:8000/](http://127.0.0.1:8000/) in your browser, and you'l
The generated home page is different from the rest: it's a welcome/marketing page. Clicking on the "Documentation" link takes you to the first page of your actual documentation.
-
+:::
## Build
When you are ready to publish your documentation, run the `python docs.py build` command, and your documentation will be generated into a `build` folder. This is a static site that can be copied and deployed anywhere.
Note that your `assets` folder will be **copied** into the build folder, so don't commit the build folder into your source code repository, because you will waste space with two copies of the same files.
-
diff --git a/docs/content/quickstart/write.md b/docs/content/quickstart/write.md
index 4bdd6ea..574beb1 100644
--- a/docs/content/quickstart/write.md
+++ b/docs/content/quickstart/write.md
@@ -16,12 +16,10 @@ It uses common Markdown syntax with many popular extensions. You can read about
Each page must have a metadata section at the beginning. This is a list of `name: value` pairs
that many tools call "Frontmatter".
-/// details | Actually...
- type: tip
-
+```{tip} Actually...
The metadata is parsed as a [restricted subset of the YAML format](https://hitchdev.com/strictyaml/)
and can also contain lists and multi-line strings.
-///
+```
To set it, add a section surrounded by `---` at the beginning of each of your pages, like this:
diff --git a/docs/content/versions.md b/docs/content/versions.md
index f4c7ca1..f41210c 100644
--- a/docs/content/versions.md
+++ b/docs/content/versions.md
@@ -75,17 +75,17 @@ Add a link to the list of options in the version selector at `views/version_sele
{ .only-dark }
-/// note
+::: note
The version selector does not render in archived versions. Otherwise, it would link only to versions that existed when created, which might not even be available anymore.
-///
+:::
### 4. Deploy
You can now copy the generated version folder along with the rest of your live documentation, so your main documentation will be at `http://example.com/`, and the documentation for the archived version will be available at `http://example.com/{VERSION}/`.
-/// warning
+::: warning
Make sure you also commit the `archive/` folder to your source code.
-///
+:::
## Managing separate "live" versions
@@ -186,8 +186,7 @@ build/
```
-/// note
-
+::: note
The prefixes don't need to be equal to the version numbers. They can be any string, for example:
```python
@@ -198,8 +197,7 @@ variants={
```
**However, they must be named like the folders in `content/`**.
-
-///
+:::
### 4. Enable the version selector
diff --git a/docs/docs.py b/docs/docs.py
index 43f2438..096b2ab 100644
--- a/docs/docs.py
+++ b/docs/docs.py
@@ -32,7 +32,7 @@
"markdown/admonitions.md",
"markdown/attributes.md",
"markdown/tabs.md",
- "markdown/html.md",
+ # "markdown/html.md",
],
},
"autodoc.md",
diff --git a/docs/views/autodoc.jinja b/docs/views/autodoc.jinja
deleted file mode 100644
index cbd78cd..0000000
--- a/docs/views/autodoc.jinja
+++ /dev/null
@@ -1,121 +0,0 @@
-{# def ds, level=2 #}
-{# import "autodoc.jinja" as Autodoc #}
-
-{%- if ds.symbol or ds.name %}
-
- {{ ds.symbol }}
- {{ ds.name }}
- {% if ds.label -%}
- {{ ds.label }}
- {%- endif %}
-
-{% endif -%}
-
-{%- if ds.short_description -%}
-
- {{ ds.short_description | markdown }}
-
-{% endif -%}
-
-{%- if ds.signature -%}
-
- {{ ds.signature | markdown("python") }}
-
-{% endif -%}
-
-{%- if ds.bases -%}
-
-
Bases:
- {%- for base in ds.bases %} {{ base }}{% if not loop.last %}, {% endif %}
- {%- endfor %}
{% endmacro %}
diff --git a/src/writeadoc/ext/jx.py b/src/writeadoc/ext/jx.py
deleted file mode 100644
index 531084e..0000000
--- a/src/writeadoc/ext/jx.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import re
-
-from markdown.extensions import Extension
-from markdown.postprocessors import Postprocessor
-from markdown.preprocessors import Preprocessor
-
-
-re_tag_name = r"[A-Z][0-9A-Za-z_.:$-]*"
-
-
-class JXTagPreprocessor(Preprocessor):
- """
- Processes HTML tags that start with uppercase letter
- and replaces them with marked divs before parsing.
- """
-
- RX_OPEN = re.compile(fr"<\s?({re_tag_name})\b")
- RE_OPEN_REPL = r'
")
- RE_CLOSE_REPL = r'
'
-
- def run(self, lines):
- new_lines = []
- for line in lines:
- line = self.RX_OPEN.sub(self.RE_OPEN_REPL, line)
- line = self.RX_CLOSE.sub(self.RE_CLOSE_REPL, line)
- new_lines.append(line)
- return new_lines
-
-
-class JXTagPostprocessor(Postprocessor):
- """
- Reverts the transformation done by JXTagProcessor
- """
-
- RX_OPEN = re.compile(fr'
(\n)?
')
- RE_CLOSE_REPL = r"\2\1>"
-
- RX_OPEN2 = re.compile(fr"<div tag="({re_tag_name})"")
- RE_OPEN2_REPL = r"<\1"
-
- RX_CLOSE2 = re.compile(fr"<meta tag="({re_tag_name})"></div>")
- RE_CLOSE2_REPL = r"</\1>"
-
- def run(self, text):
- # Restore tags
- text = self.RX_OPEN.sub(self.RE_OPEN_REPL, text)
- text = self.RX_CLOSE.sub(self.RE_CLOSE_REPL, text)
-
- # Restore tags that were escaped by Markdown (inside code blocks, etc.)
- text = self.RX_OPEN2.sub(self.RE_OPEN2_REPL, text)
- text = self.RX_CLOSE2.sub(self.RE_CLOSE2_REPL, text)
-
- return text
-
-
-class JXExtension(Extension):
- """
- Python Markdown extension to handle JX tags
- """
-
- def extendMarkdown(self, md):
- md.preprocessors.register(JXTagPreprocessor(md), "jx_marker", 40)
- md.postprocessors.register(JXTagPostprocessor(md), "jx_restore", 10)
-
-
-def makeExtension(**kwargs):
- return JXExtension(**kwargs)
diff --git a/src/writeadoc/ext/pagetoc.py b/src/writeadoc/ext/pagetoc.py
deleted file mode 100644
index b6fa385..0000000
--- a/src/writeadoc/ext/pagetoc.py
+++ /dev/null
@@ -1,375 +0,0 @@
-"""
-Custom Table of Contents extension for Markdown.
-Extended to support skipping headers with a `skip-toc` attribute
-and to remove the HTML generation.
-
-Original code Copyright 2008 [Jack Miller](https://codezen.org/)
-Python-Markdown changes Copyright 2008-2024 The Python Markdown Project
-License: [BSD](https://opensource.org/licenses/bsd-license.php)
-"""
-
-import html
-import re
-import unicodedata
-import xml.etree.ElementTree as etree
-from collections.abc import MutableSet
-from copy import deepcopy
-from typing import Any
-
-from markdown import Markdown
-from markdown.extensions import Extension
-from markdown.serializers import RE_AMP
-from markdown.treeprocessors import Treeprocessor, UnescapeTreeprocessor
-from markdown.util import (
- AMP_SUBSTITUTE,
- parseBoolValue,
-)
-
-
-def slugify(value: str, separator: str, unicode: bool = False) -> str:
- """Slugify a string, to make it URL friendly."""
- if not unicode:
- # Replace Extended Latin characters with ASCII, i.e. `žlutý` => `zluty`
- value = unicodedata.normalize("NFKD", value)
- value = value.encode("ascii", "ignore").decode("ascii")
- value = re.sub(r"[^\w\s-]", "", value).strip().lower()
- return re.sub(r"[{}\s]+".format(separator), separator, value)
-
-
-def slugify_unicode(value: str, separator: str) -> str:
- """Slugify a string, to make it URL friendly while preserving Unicode characters."""
- return slugify(value, separator, unicode=True)
-
-
-IDCOUNT_RE = re.compile(r"^(.*)_([0-9]+)$")
-
-
-def unique(id: str, ids: MutableSet[str]) -> str:
- """Ensure id is unique in set of ids. Append '_1', '_2'... if not"""
- while id in ids or not id:
- m = IDCOUNT_RE.match(id)
- if m:
- id = "%s_%d" % (m.group(1), int(m.group(2)) + 1)
- else:
- id = "%s_%d" % (id, 1)
- ids.add(id)
- return id
-
-
-def unescape(text: str) -> str:
- """Unescape Markdown backslash escaped text."""
- c = UnescapeTreeprocessor()
- return c.unescape(text)
-
-
-def strip_tags(text: str) -> str:
- """Strip HTML tags and return plain text. Note: HTML entities are unaffected."""
- # A comment could contain a tag, so strip comments first
- while (start := text.find("", start)) != -1:
- text = f"{text[:start]}{text[end + 3 :]}"
-
- while (start := text.find("<")) != -1 and (end := text.find(">", start)) != -1:
- text = f"{text[:start]}{text[end + 1 :]}"
-
- # Collapse whitespace
- text = " ".join(text.split())
- return text
-
-
-def escape_cdata(text: str) -> str:
- """Escape character data."""
- if "&" in text:
- # Only replace & when not part of an entity
- text = RE_AMP.sub("&", text)
- if "<" in text:
- text = text.replace("<", "<")
- if ">" in text:
- text = text.replace(">", ">")
- return text
-
-
-def run_postprocessors(text: str, md: Markdown) -> str:
- """Run postprocessors from Markdown instance on text."""
- for pp in md.postprocessors:
- text = pp.run(text)
- return text.strip()
-
-
-def render_inner_html(el: etree.Element, md: Markdown) -> str:
- """Fully render inner html of an `etree` element as a string."""
- # The `UnescapeTreeprocessor` runs after `toc` extension so run here.
- text = unescape(md.serializer(el))
-
- # strip parent tag
- start = text.index(">") + 1
- end = text.rindex("<")
- text = text[start:end].strip()
-
- return run_postprocessors(text, md)
-
-
-def remove_fnrefs(root: etree.Element) -> etree.Element:
- """Remove footnote references from a copy of the element, if any are present."""
- # Remove footnote references, which look like this: `...`.
- # If there are no `sup` elements, then nothing to do.
- if next(root.iter("sup"), None) is None:
- return root
- root = deepcopy(root)
- # Find parent elements that contain `sup` elements.
- for parent in root.findall(".//sup/.."):
- carry_text = ""
- for child in reversed(
- parent
- ): # Reversed for the ability to mutate during iteration.
- # Remove matching footnote references but carry any `tail` text to preceding elements.
- if child.tag == "sup" and child.get("id", "").startswith("fnref"):
- carry_text = f"{child.tail or ''}{carry_text}"
- parent.remove(child)
- elif carry_text:
- child.tail = f"{child.tail or ''}{carry_text}"
- carry_text = ""
- if carry_text:
- parent.text = f"{parent.text or ''}{carry_text}"
- return root
-
-
-def nest_toc_tokens(toc_list):
- """Given an unsorted list with errors and skips, return a nested one.
-
- [{'level': 1}, {'level': 2}]
- =>
- [{'level': 1, 'children': [{'level': 2, 'children': []}]}]
-
- A wrong list is also converted:
-
- [{'level': 2}, {'level': 1}]
- =>
- [{'level': 2, 'children': []}, {'level': 1, 'children': []}]
- """
-
- ordered_list = []
- if len(toc_list):
- # Initialize everything by processing the first entry
- last = toc_list.pop(0)
- last["children"] = []
- levels = [last["level"]]
- ordered_list.append(last)
- parents = []
-
- # Walk the rest nesting the entries properly
- while toc_list:
- t = toc_list.pop(0)
- current_level = t["level"]
- t["children"] = []
-
- # Reduce depth if current level < last item's level
- if current_level < levels[-1]:
- # Pop last level since we know we are less than it
- levels.pop()
-
- # Pop parents and levels we are less than or equal to
- to_pop = 0
- for p in reversed(parents):
- if current_level <= p["level"]:
- to_pop += 1
- else: # pragma: no cover
- break
- if to_pop:
- levels = levels[:-to_pop]
- parents = parents[:-to_pop]
-
- # Note current level as last
- levels.append(current_level)
-
- # Level is the same, so append to
- # the current parent (if available)
- if current_level == levels[-1]:
- (parents[-1]["children"] if parents else ordered_list).append(t)
-
- # Current level is > last item's level,
- # So make last item a parent and append current as child
- else:
- last["children"].append(t)
- parents.append(last)
- levels.append(current_level)
- last = t
-
- return ordered_list
-
-
-class TocTreeprocessor(Treeprocessor):
- """Step through document and build TOC."""
-
- def __init__(self, md: Markdown, config: dict[str, Any]):
- super().__init__(md)
-
- self.title: str = config["title"]
- self.base_level = int(config["baselevel"]) - 1
- self.slugify = config["slugify"]
- self.sep = config["separator"]
- self.toc_class = config["toc_class"]
- self.title_class: str = config["title_class"]
- self.use_anchors: bool = parseBoolValue(config["anchorlink"]) # type: ignore
- self.anchorlink_class: str = config["anchorlink_class"]
- self.use_permalinks = parseBoolValue(config["permalink"], False)
- if self.use_permalinks is None:
- self.use_permalinks = config["permalink"]
- self.permalink_class: str = config["permalink_class"]
- self.permalink_title: str = config["permalink_title"]
- self.permalink_leading: bool | None = parseBoolValue(
- config["permalink_leading"], False
- )
- self.header_rgx = re.compile("[Hh][123456]")
- if isinstance(config["toc_depth"], str) and "-" in config["toc_depth"]:
- self.toc_top, self.toc_bottom = [
- int(x) for x in config["toc_depth"].split("-")
- ]
- else:
- self.toc_top = 1
- self.toc_bottom = int(config["toc_depth"])
-
- def set_level(self, elem: etree.Element) -> None:
- """Adjust header level according to base level."""
- level = int(elem.tag[-1]) + self.base_level
- if level > 6:
- level = 6
- elem.tag = "h%d" % level
-
- def add_anchor(self, c: etree.Element, elem_id: str) -> None:
- anchor = etree.Element("a")
- anchor.text = c.text
- anchor.attrib["href"] = "#" + elem_id
- anchor.attrib["class"] = self.anchorlink_class
- c.text = ""
- for elem in c:
- anchor.append(elem)
- while len(c):
- c.remove(c[0])
- c.append(anchor)
-
- def add_permalink(self, c: etree.Element, elem_id: str) -> None:
- permalink = etree.Element("a")
- permalink.text = (
- f"{AMP_SUBSTITUTE}para;"
- if self.use_permalinks is True
- else self.use_permalinks
- ) # type: ignore
- permalink.attrib["href"] = "#" + elem_id
- permalink.attrib["class"] = self.permalink_class
- if self.permalink_title:
- permalink.attrib["title"] = self.permalink_title
- if self.permalink_leading:
- permalink.tail = c.text
- c.text = ""
- c.insert(0, permalink)
- else:
- c.append(permalink)
-
- def run(self, doc: etree.Element) -> None:
- # Get a list of id attributes
- used_ids = set()
- for el in doc.iter():
- if "id" in el.attrib:
- used_ids.add(el.attrib["id"])
-
- toc_tokens = []
- for el in doc.iter():
- if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
- if "skip-toc" in el.attrib:
- continue
-
- self.set_level(el)
- innerhtml = render_inner_html(remove_fnrefs(el), self.md)
- name = strip_tags(innerhtml)
-
- # Do not override pre-existing ids
- if "id" not in el.attrib:
- el.attrib["id"] = unique(
- self.slugify(html.unescape(name), self.sep), used_ids
- )
-
- if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom:
- toc_tokens.append(
- {
- "level": int(el.tag[-1]),
- "id": unescape(el.attrib["id"]),
- "name": name,
- "html": innerhtml,
- }
- )
-
- if self.use_anchors:
- self.add_anchor(el, el.attrib["id"])
- if self.use_permalinks not in [False, None]:
- self.add_permalink(el, el.attrib["id"])
-
- toc_tokens = nest_toc_tokens(toc_tokens)
- self.md.toc_tokens = toc_tokens # type: ignore
-
-
-class TocExtension(Extension):
- TreeProcessorClass = TocTreeprocessor
-
- def __init__(self, **kwargs):
- self.config = {
- "title": ["", "Title to insert into TOC `
`. Default: an empty string."],
- "title_class": [
- "toctitle",
- "CSS class used for the title. Default: `toctitle`.",
- ],
- "toc_class": ["toc", "CSS class(es) used for the link. Default: `toclink`."],
- "anchorlink": [
- False,
- "True if header should be a self link. Default: `False`.",
- ],
- "anchorlink_class": [
- "toclink",
- "CSS class(es) used for the link. Defaults: `toclink`.",
- ],
- "permalink": [
- True,
- "True or link text if a Sphinx-style permalink should be added. Default: `False`.",
- ],
- "permalink_class": [
- "headerlink",
- "CSS class(es) used for the link. Default: `headerlink`.",
- ],
- "permalink_title": [
- "Permanent link",
- "Title attribute of the permalink. Default: `Permanent link`.",
- ],
- "permalink_leading": [
- False,
- "True if permalinks should be placed at start of the header, rather than end. Default: False.",
- ],
- "baselevel": ["1", "Base level for headers. Default: `1`."],
- "slugify": [
- slugify,
- "Function to generate anchors based on header text. Default: `slugify`.",
- ],
- "separator": ["-", "Word separator. Default: `-`."],
- "toc_depth": [
- "1-3",
- "Define the range of section levels to include in the Table of Contents. A single integer "
- "(b) defines the bottom section level (
..) only. A string consisting of two digits "
- "separated by a hyphen in between (`2-5`) defines the top (t) and the bottom (b) (..). "
- "Default: `6` (bottom).",
- ],
- }
- """ Default configuration options. """
-
- super().__init__(**kwargs)
-
- def extendMarkdown(self, md):
- md.registerExtension(self)
- self.md = md
- self.reset()
- tocext = self.TreeProcessorClass(md, self.getConfigs())
- md.treeprocessors.register(tocext, "pagetoc", 5)
-
- def reset(self) -> None:
- self.md.toc_tokens = [] # type: ignore
-
-
-def makeExtension(**kwargs): # pragma: no cover
- return TocExtension(**kwargs)
diff --git a/src/writeadoc/ext/tab.py b/src/writeadoc/ext/tab.py
deleted file mode 100644
index 2266258..0000000
--- a/src/writeadoc/ext/tab.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""
-Tab Block extension for Markdown.
-
-Original code Copyright 2008-2024 The Python Markdown Project
-https://github.com/facelessuser/pymdown-extensions/blob/main/pymdownx/blocks/tab.py
-Used under the MIT License
-"""
-
-import xml.etree.ElementTree as etree
-
-from markdown.treeprocessors import Treeprocessor
-from pymdownx.blocks import BlocksExtension
-from pymdownx.blocks.block import Block, type_boolean
-
-
-HEADERS = {"h1", "h2", "h3", "h4", "h5", "h6"}
-
-
-class TabbedTreeprocessor(Treeprocessor):
- """Tab tree processor."""
-
- def run(self, doc):
- # Get a list of id attributes
- used_ids = set()
- for el in doc.iter():
- if "id" in el.attrib:
- used_ids.add(el.attrib["id"])
-
-
-class Tab(Block):
- """
- Tabbed container.
-
- Arguments:
- - A tab title.
-
- Options:
- - `new` (boolean): since consecutive tabs are automatically grouped, `new` can force a tab
- to start a new tab container.
-
- Content:
- Detail body.
- """
-
- NAME = "tab"
-
- ARGUMENT = True
- OPTIONS = {"new": (False, type_boolean), "select": (False, type_boolean)}
-
- def on_init(self):
- """Handle initialization."""
-
- # Track tab group count across the entire page.
- if "tab_group_count" not in self.tracker:
- self.tracker["tab_group_count"] = 0
-
- self.tab_content = None
-
- def last_child(self, parent):
- """Return the last child of an `etree` element."""
-
- if len(parent):
- return parent[-1]
- else:
- return None
-
- def on_add(self, block):
- """Adjust where the content is added."""
-
- if self.tab_content is None:
- for d in block.findall("div"):
- c = d.attrib["class"]
- if c == "tabbed-content" or c.startswith("tabbed-content "):
- self.tab_content = list(d)[-1]
- break
-
- return self.tab_content
-
- def on_create(self, parent):
- """Create the element."""
-
- new_group = self.options["new"]
- select = self.options["select"]
- title = self.argument
- sibling = self.last_child(parent)
- tabbed_set = "tabbed-set"
- index = 0
- labels = None
- content = None
-
- if (
- sibling is not None
- and sibling.tag.lower() == "div"
- and sibling.attrib.get("class", "") == tabbed_set
- and not new_group
- ):
- first = False
- tab_group = sibling
-
- index = [index for index, _ in enumerate(tab_group.findall("input"), 1)][-1]
- for d in tab_group.findall("div"):
- if d.attrib["class"] == "tabbed-labels":
- labels = d
- elif d.attrib["class"] == "tabbed-content":
- content = d
- if labels is not None and content is not None:
- break
- else:
- first = True
- self.tracker["tab_group_count"] += 1
- tab_group = etree.SubElement(
- parent,
- "div",
- {
- "class": tabbed_set,
- "data-tabs": "%d:0" % self.tracker["tab_group_count"],
- },
- )
- labels = etree.SubElement(tab_group, "div", {"class": "tabbed-labels"})
- content = etree.SubElement(tab_group, "div", {"class": "tabbed-content"})
-
- data = tab_group.attrib["data-tabs"].split(":")
- tab_set = int(data[0])
- tab_count = int(data[1]) + 1
-
- attributes = {
- "name": "__tabbed_%d" % tab_set,
- "type": "radio",
- "id": "__tabbed_%d_%d" % (tab_set, tab_count),
- }
- attributes2 = {"for": "__tabbed_%d_%d" % (tab_set, tab_count)}
-
- if first or select:
- attributes["checked"] = "checked"
- # Remove any previously assigned "checked states" to siblings
- for i in tab_group.findall("input"):
- if i.attrib.get("name", "") == f"__tabbed_{tab_set}":
- if "checked" in i.attrib:
- del i.attrib["checked"]
-
- input_el = etree.Element("input", attributes)
- tab_group.insert(index, input_el)
- lab = etree.SubElement(labels, "label", attributes2) # type: ignore
- lab.text = title
- attrib = {"class": "tabbed-block"}
- etree.SubElement(content, "div", attrib) # type: ignore
-
- tab_group.attrib["data-tabs"] = "%d:%d" % (tab_set, tab_count)
-
- return tab_group
-
-
-class TabExtension(BlocksExtension):
- """Tab Block Extension."""
-
- def extendMarkdownBlocks(self, md, block_mgr):
- block_mgr.register(Tab, self.getConfigs())
-
-
-def makeExtension(*args, **kwargs):
- """Return extension."""
-
- return TabExtension(*args, **kwargs)
diff --git a/src/writeadoc/main.py b/src/writeadoc/main.py
index 90e461f..609c818 100644
--- a/src/writeadoc/main.py
+++ b/src/writeadoc/main.py
@@ -8,29 +8,26 @@
from multiprocessing import Process
from pathlib import Path
from tempfile import mkdtemp
-from textwrap import dedent
+# from textwrap import dedent
import jx
-import markdown
from markupsafe import Markup
from . import utils
from .pages import PagesProcessor
-from .types import PageData, SiteData, TUserPages
+from .types import PageData, SiteData
from .utils import get_random_messages, logger
class Docs:
- pages: TUserPages
+ pages: list[str | dict[str, t.Any]]
site: SiteData
prefix: str = ""
- variants: dict[str, t.Self]
+ variants: "dict[str, Docs]"
is_main: bool = True
skip_home: bool = False
strings: dict[str, str]
-
- md_filter_renderer: markdown.Markdown
catalog: jx.Catalog
root_dir: Path
@@ -46,10 +43,10 @@ def __init__(
root: str,
/,
*,
- pages: TUserPages,
+ pages: list[str | dict[str, t.Any]],
site: dict[str, t.Any] | None = None,
prefix: str = "",
- variants: dict[str, t.Self] | None = None,
+ variants: "dict[str, Docs] | None" = None,
skip_home: bool = False,
):
"""
@@ -90,17 +87,7 @@ def __init__(
self.pages_processor = PagesProcessor(self)
- self.md_filter_renderer = markdown.Markdown(
- extensions=[*utils.DEFAULT_MD_EXTENSIONS],
- extension_configs={**utils.DEFAULT_MD_CONFIG},
- output_format="html",
- tab_length=2,
- )
-
self.catalog = jx.Catalog(
- filters={
- "markdown": self.markdown_filter
- },
site=self.site,
docs=self,
_=self.translate,
@@ -136,7 +123,7 @@ def cli(self):
"--llm",
action="store_true",
default=False,
- help="Generate a `LLM.txt` file with all the markdown content",
+ help=f"Generate a `{self.site.name}.txt` file with all the markdown content",
)
args = parser.parse_args()
@@ -218,7 +205,7 @@ def build(self, *, devmode: bool = True, llm: bool = False) -> None:
print(f"{messages[2]}...")
if llm:
- print("Building LLM.txt...")
+ print(f"Building {self.site.name}.txt...")
self._render_llm_file()
self._render_search_page()
@@ -235,15 +222,6 @@ def build(self, *, devmode: bool = True, llm: bool = False) -> None:
print("Fingerprinting assets URLs...")
self._fingerprint_assets()
- def markdown_filter(self, source: str, code: str = "") -> str:
- source = dedent(source.strip("\n")).strip()
- if code:
- source = f"\n```{code}\n{source}\n```\n"
- self.md_filter_renderer.reset()
- html = self.md_filter_renderer.convert(source).strip()
- html = html.replace("
", "
")
- return Markup(html)
-
def translate(self, key: str, **kwargs) -> str:
"""
Translate a key using the strings dictionary.
@@ -353,12 +331,12 @@ def _render_extra(self) -> None:
self.log(outpath)
def _render_llm_file(self) -> None:
- outpath = self.build_dir / self.prefix / "LLM.txt"
+ outpath = self.build_dir / self.prefix / f"{self.site.name}.txt"
outpath.parent.mkdir(parents=True, exist_ok=True)
try:
body = self.catalog.render("llm.jinja")
except jx.JxException as err:
- raise RuntimeError("Error rendering LLM.txt") from err
+ raise RuntimeError(f"Error rendering {self.site.name}.txt") from err
outpath.write_text(body, encoding="utf-8")
self.log(outpath)
diff --git a/src/writeadoc/md/__init__.py b/src/writeadoc/md/__init__.py
new file mode 100644
index 0000000..05ba13d
--- /dev/null
+++ b/src/writeadoc/md/__init__.py
@@ -0,0 +1 @@
+from .render import render_markdown # noqa
\ No newline at end of file
diff --git a/src/writeadoc/md/admonition.py b/src/writeadoc/md/admonition.py
new file mode 100644
index 0000000..cf8edd7
--- /dev/null
+++ b/src/writeadoc/md/admonition.py
@@ -0,0 +1,82 @@
+import re
+import typing as t
+
+from mistune.directives._base import BaseDirective, DirectivePlugin
+
+from .utils import render_attrs
+
+
+if t.TYPE_CHECKING:
+ from mistune.block_parser import BlockParser
+ from mistune.core import BlockState
+ from mistune.markdown import Markdown
+
+
+class Admonition(DirectivePlugin):
+ SUPPORTED_NAMES = {
+ "note",
+ "tip",
+ "warning",
+ "error",
+ "new",
+ }
+
+ def parse(
+ self, block: "BlockParser", m: re.Match[str], state: "BlockState"
+ ) -> dict[str, t.Any]:
+ name = self.parse_type(m)
+ attrs = dict(self.parse_options(m))
+ attrs.setdefault("class", "")
+ attrs["class"] += f"admonition {name} {attrs['class']}".strip()
+
+ title = self.parse_title(m)
+ if not title:
+ title = name.capitalize()
+
+ content = self.parse_content(m)
+ children = [
+ {
+ "type": "admonition_title",
+ "text": title,
+ "attrs": attrs,
+ },
+ {
+ "type": "admonition_content",
+ "children": self.parse_tokens(block, content, state),
+ },
+ ]
+ return {
+ "type": "admonition",
+ "children": children,
+ "attrs": attrs,
+ }
+
+ def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
+ for name in self.SUPPORTED_NAMES:
+ directive.register(name, self.parse)
+
+ assert md.renderer is not None
+ if md.renderer.NAME == "html":
+ md.renderer.register("admonition", render_admonition)
+ md.renderer.register("admonition_title", render_admonition_title)
+ md.renderer.register("admonition_content", render_admonition_content)
+
+
+def render_admonition(self: t.Any, text: str, **attrs: t.Any) -> str:
+ html_attrs = render_attrs(attrs)
+
+ if "open" in attrs:
+ return f"\n{text}\n"
+ else:
+ return f"\n{text}\n"
+
+
+def render_admonition_title(self: t.Any, text: str, **attrs: t.Any) -> str:
+ if "open" in attrs:
+ return f'{text}\n'
+ else:
+ return f'
\n"
+
+ def heading(self, text: str, level: int, **attrs: t.Any) -> str:
+ return f"{text}\n"
+
+ def thematic_break(self, **attrs: t.Any) -> str:
+ return f"\n"
+
+ def block_quote(self, text: str, **attrs: t.Any) -> str:
+ # The attributes are not parsed for block quotes, but we allow them
+ # to be passed and rendered for future compatibility.
+ return f"
{text}
\n"
+
+ def list(self, text: str, ordered: bool, **attrs: t.Any) -> str:
+ # The attributes are not parsed for lists, but we allow them
+ # to be passed and rendered for future compatibility.
+ if ordered:
+ return f"\n{text}\n"
+ return f"
\n{text}
\n"
+
+ def list_item(self, text: str, **attrs: t.Any) -> str:
+ # The attributes are not parsed for list items, but we allow them
+ # to be passed and rendered for future compatibility.
+ return f"
{text}
\n"
+
+ # For the methods below, allow attributes
+ # (possible with syntax errors) but ignore them
+
+ def text(self, text: str, **attrs: t.Any) -> str:
+ return super().text(text)
+
+ def linebreak(self, **attrs: t.Any) -> str:
+ return " \n"
+
+ def softbreak(self, **attrs: t.Any) -> str:
+ return "\n"
+
+ def inline_html(self, html: str, **attrs: t.Any) -> str:
+ return super().inline_html(html)
+
+ def block_html(self, html: str, **attrs: t.Any) -> str:
+ return super().block_html(html)
+
+ def blank_line(self, **attrs: t.Any) -> str:
+ return ""
+
+ def block_text(self, text: str, **attrs: t.Any) -> str:
+ return text
diff --git a/src/writeadoc/md/render.py b/src/writeadoc/md/render.py
new file mode 100644
index 0000000..556c89b
--- /dev/null
+++ b/src/writeadoc/md/render.py
@@ -0,0 +1,77 @@
+import re
+import typing as t
+import unicodedata
+from collections.abc import MutableMapping
+
+import mistune
+from mistune.directives import Include, TableOfContents
+from mistune.plugins.abbr import abbr
+from mistune.plugins.def_list import def_list
+from mistune.plugins.footnotes import footnotes
+from mistune.plugins.table import table
+from mistune.plugins.task_lists import task_lists
+from mistune.toc import add_toc_hook
+
+from .admonition import Admonition
+from .attrs import block_attrs, inline_attrs
+from .block_directive import BlockDirective
+from .div import Container
+from .figure import Figure
+from .formatting import insert, mark, strikethrough, subscript, superscript
+from .html_renderer import HTMLRenderer
+from .tab import Tab
+
+
+md = mistune.Markdown(
+ HTMLRenderer(escape=False),
+ plugins=[
+ abbr,
+ def_list,
+ footnotes,
+ table,
+ task_lists,
+ #
+ block_attrs,
+ inline_attrs,
+ insert,
+ mark,
+ strikethrough,
+ subscript,
+ superscript,
+ # md_in_html, ???
+ BlockDirective([
+ Include(),
+ TableOfContents(),
+ #
+ Admonition(),
+ Container(),
+ Figure(),
+ Tab(),
+ ]),
+ ]
+)
+
+
+def slugify(value: str, separator: str = "-", unicode: bool = True) -> str:
+ """Slugify a string, to make it URL friendly."""
+ if not unicode:
+ # Replace Extended Latin characters with ASCII, i.e. `žlutý` => `zluty`
+ value = unicodedata.normalize("NFKD", value)
+ value = value.encode("ascii", "ignore").decode("ascii")
+ value = re.sub(r"[^\w\s-]", "", value).strip().lower()
+ return re.sub(r"[{}\s]+".format(separator), separator, value)
+
+
+def heading_id(token: dict[str, t.Any], index: int) -> str:
+ return slugify(token["text"])
+
+
+add_toc_hook(md, heading_id=heading_id)
+
+
+def render_markdown(source: str, **kwargs: t.Any) -> tuple[str, MutableMapping]:
+ """Render the given Markdown source to HTML using the mistune renderer."""
+ state = mistune.BlockState()
+ state.env.update(kwargs)
+ html, state = md.parse(source, state=state)
+ return str(html), state.env
diff --git a/src/writeadoc/md/tab.py b/src/writeadoc/md/tab.py
new file mode 100644
index 0000000..5bc4cda
--- /dev/null
+++ b/src/writeadoc/md/tab.py
@@ -0,0 +1,188 @@
+import re
+import typing as t
+
+import mistune
+from mistune.directives._base import BaseDirective, DirectivePlugin
+
+
+if t.TYPE_CHECKING:
+ from mistune.block_parser import BlockParser
+ from mistune.core import BlockState
+ from mistune.markdown import Markdown
+
+
+class Tab(DirectivePlugin):
+ """Tab directive for creating tabbed content panels.
+
+ Syntax:
+ ::: tab | Label with **markdown** support
+ Content here with **markdown** support
+ :::
+
+ Consecutive tab directives are automatically grouped into a tabbed set.
+ """
+
+ def parse(
+ self, block: "BlockParser", m: re.Match[str], state: "BlockState"
+ ) -> dict[str, t.Any]:
+ label = self.parse_title(m)
+ content = self.parse_content(m)
+ attrs = dict(self.parse_options(m))
+
+ return {
+ "type": "tab",
+ "label": label, # Raw text - will be inline-parsed during grouping
+ "children": self.parse_tokens(block, content, state),
+ "attrs": attrs,
+ }
+
+ def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
+ directive.register("tab", self.parse)
+
+ if md.renderer and md.renderer.NAME == "html":
+ md.renderer.register("tab", render_tab)
+ md.renderer.register("tabbed_set", render_tabbed_set)
+
+ # Register the grouping hook (runs before rendering)
+ def hook(markdown: "Markdown", state: "BlockState") -> None:
+ _group_tabs_hook(markdown, state)
+
+ md.before_render_hooks.append(hook)
+
+
+def render_tab(self: t.Any, text: str, **attrs: t.Any) -> str:
+ """Tab tokens are rendered by their parent tabbed_set, not individually."""
+ return ""
+
+
+def render_tabbed_set(
+ self: t.Any,
+ text: str,
+ tabs: list,
+ set_id: int,
+ **attrs: t.Any,
+) -> str:
+ """Render a complete tabbed set from pre-rendered tab data."""
+ inputs = []
+ labels = []
+ panels = []
+
+ # Find which tab should be selected (default to first)
+ selected_index = 0
+ for i, tab in enumerate(tabs):
+ if tab.get("select"):
+ selected_index = i
+ break
+
+ for i, tab in enumerate(tabs):
+ tab_id = f"__tabbed_{set_id}_{i + 1}"
+ checked = " checked" if i == selected_index else ""
+
+ inputs.append(
+ f''
+ )
+ labels.append(f'')
+ panels.append(f'
\n{tab["content_html"]}
')
+
+ return (
+ '
\n'
+ + "\n".join(inputs)
+ + "\n"
+ + '
\n'
+ + "\n".join(labels)
+ + "\n
\n"
+ + '
\n'
+ + "\n".join(panels)
+ + "\n
\n"
+ + "
\n"
+ )
+
+
+def _group_tabs_hook(md: "Markdown", state: "BlockState") -> None:
+ """Before-render hook that groups consecutive tab tokens into tabbed_set containers."""
+ state.tokens = _group_consecutive_tabs(state.tokens, state, md)
+
+
+def _group_consecutive_tabs(
+ tokens: list[dict], state: "BlockState", md: "Markdown"
+) -> list[dict]:
+ """Transform token list to group consecutive tab tokens into tabbed_set containers."""
+ result = []
+ tab_buffer: list[dict] = []
+ # Buffer blank lines that appear between tabs - they get discarded if followed by another tab
+ blank_buffer: list[dict] = []
+
+ for token in tokens:
+ if token["type"] == "tab":
+ # Check if this tab should start a new group
+ if token["attrs"].get("new") and tab_buffer:
+ result.append(_create_tabbed_set(tab_buffer, state, md))
+ tab_buffer = []
+ # Discard blank lines between tabs
+ blank_buffer = []
+ tab_buffer.append(token)
+ elif token["type"] == "blank_line" and tab_buffer:
+ # Potentially between tabs - buffer it
+ blank_buffer.append(token)
+ else:
+ # Non-tab, non-blank token
+ if tab_buffer:
+ result.append(_create_tabbed_set(tab_buffer, state, md))
+ tab_buffer = []
+ # Add back any buffered blank lines (they weren't between tabs)
+ result.extend(blank_buffer)
+ blank_buffer = []
+ # Recursively process children (e.g., tabs inside admonitions)
+ if "children" in token:
+ token["children"] = _group_consecutive_tabs(token["children"], state, md)
+ result.append(token)
+
+ # Flush remaining tabs at end
+ if tab_buffer:
+ result.append(_create_tabbed_set(tab_buffer, state, md))
+ # Any trailing blank lines after tabs are discarded
+
+ return result
+
+
+def _create_tabbed_set(tabs: list[dict], state: "BlockState", md: "Markdown") -> dict:
+ """Create a tabbed_set token from a list of tab tokens."""
+ assert md.renderer is not None
+
+ counter = state.env.setdefault("_tab_set_counter", 0) + 1
+ state.env["_tab_set_counter"] = counter
+
+ rendered_tabs = []
+ for tab in tabs:
+ # Render label as inline markdown
+ if tab["label"]:
+ inline_state = mistune.InlineState({})
+ inline_state.src = tab["label"]
+ label_tokens = md.inline.parse(inline_state)
+ label_html = md.renderer.render_tokens(label_tokens, state)
+ else:
+ label_html = ""
+
+ # Process children with _iter_render to convert 'text' to 'children'
+ # This is necessary because before_render_hooks runs before _iter_render
+ processed_children = list(md._iter_render(tab["children"], state))
+
+ # Render content children as HTML
+ content_html = md.renderer.render_tokens(processed_children, state)
+
+ rendered_tabs.append(
+ {
+ "label_html": label_html,
+ "content_html": content_html,
+ "select": tab["attrs"].get("select"),
+ }
+ )
+
+ return {
+ "type": "tabbed_set",
+ "children": [], # Empty - Mistune won't auto-render
+ "attrs": {
+ "set_id": counter,
+ "tabs": rendered_tabs,
+ },
+ }
diff --git a/src/writeadoc/md/utils.py b/src/writeadoc/md/utils.py
new file mode 100644
index 0000000..662e9a1
--- /dev/null
+++ b/src/writeadoc/md/utils.py
@@ -0,0 +1,54 @@
+from mistune.util import escape_url, safe_entity
+
+
+URL_ATTRS = ("href", "src", "action", "formaction")
+TRUTHY_VALUES = ("True", "true",)
+FALSY_VALUES = ("False", "false")
+
+
+def quote(text: str) -> str:
+ if '"' in text:
+ if "'" in text:
+ text = text.replace('"', """)
+ return f'"{text}"'
+ else:
+ return f"'{text}'"
+ return f'"{text}"'
+
+
+def escape_value(name: str, value: str) -> str:
+ """Escape attribute value."""
+ if name in URL_ATTRS:
+ value = escape_url(value)
+ else:
+ value = safe_entity(value)
+ return value
+
+
+def render_attrs(attrs: dict[str, str | int]) -> str:
+ """Render a dictionary of attributes to a string suitable for HTML attributes."""
+ properties = set()
+ attributes = {}
+ for name, value in attrs.items():
+ name = name.replace("_", "-")
+ str_value = str(value).lower()
+ if str_value.lower() == "false":
+ continue
+ if str_value == "true":
+ properties.add(name)
+ else:
+ attributes[name] = escape_value(name, str(value))
+
+ attributes = dict(sorted(attributes.items()))
+
+ html_attrs = [
+ f"{name}={quote(str(value))}"
+ for name, value in attributes.items()
+ ]
+ html_attrs.extend(sorted(properties))
+
+ if html_attrs:
+ return f" {' '.join(html_attrs)}"
+ else:
+ return ""
+
diff --git a/src/writeadoc/pages.py b/src/writeadoc/pages.py
index b6bc5d1..80d42b8 100644
--- a/src/writeadoc/pages.py
+++ b/src/writeadoc/pages.py
@@ -1,21 +1,18 @@
-import re
import typing as t
+from collections.abc import MutableMapping, Sequence
from pathlib import Path
from uuid import uuid4
-import jx
-import markdown
from markupsafe import Markup
from . import search, utils
-from .autodoc import Autodoc
+from .autodoc import render_autodoc
+from .md import render_markdown
from .types import (
NavItem,
PageData,
PageRef,
TMetadata,
- TUserPages,
- TUserSection,
)
from .utils import logger
@@ -24,31 +21,17 @@
from .main import Docs
-RX_AUTODOC = re.compile(r"
+"""
+ ),
+
+ ( # thematic_break
+ """
+----
+{ .fancy }
+""",
+ """
+"""
+ ),
+
+ ( # block_quote (ignore attrs)
+ """
+> This is the first line of the quote.
+> This is the second line of the quote.
+{ .fancy }
+""",
+ """
This is the first line of the quote.
+This is the second line of the quote.
+
+
+"""
+ ),
+
+ ( # ul list (ignore attrs)
+ """
+* One
+* Two
+* Three
+{ .fancy }
+""",
+ """
+
One
+
Two
+
Three
+
+
+"""
+ ),
+
+ ( # ol list (ignore attrs)
+ """
+1. One
+2. Two
+3. Three
+{ .fancy }
+""",
+ """
+