diff --git a/Makefile b/Makefile index 7a40a42..2f98ddb 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,9 @@ lint: uv run ruff check . uv run ty check -# .PHONY: coverage -# coverage: -# uv run pytest --cov-config=pyproject.toml --cov-report html --cov writeadoc src/writeadoc tests +.PHONY: coverage +coverage: + uv run pytest --cov-config=pyproject.toml --cov-report html --cov writeadoc src/writeadoc tests .PHONY: docs docs: diff --git a/docs/assets/css/base.css b/docs/assets/css/base.css index e29beda..062f975 100644 --- a/docs/assets/css/base.css +++ b/docs/assets/css/base.css @@ -225,6 +225,9 @@ ol, ul { :is(ol,ul) :is(ol,ul) ul { list-style: square; } +li [type="checkbox"] { + margin-right: 0.5em; +} ul { list-style: disc @@ -1027,8 +1030,7 @@ html.cs-dark img.invert { /******* Page - Admonitions *******/ -.page__content .admonition, -.page__content details { +.page__content .admonition { --admonition-rgb: 43, 127, 255; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); @@ -1043,50 +1045,24 @@ html.cs-dark img.invert { page-break-inside: avoid; } -@media screen and (min-width: 60em) { - .page__content .admonition.left:only-child, - .page__content details.left:only-child, - .page__content .admonition.right:only-child, - .page__content details.right:only-child { - margin-top: 0; - } - .page__content .admonition.left, - .page__content details.left { - float: left; - margin: 1em 1em 1em 0; - width: 40%; - } - .page__content .admonition.right, - .page__content details.right { - float: right; - margin: 1em 0 1em 1em; - width: 40%; - } -} @media print { - .page__content .admonition, - .page__content details { + .page__content .admonition { box-shadow: none; } } -.page__content .admonition>*, -.page__content details>* { +.page__content .admonition > * { margin-top: 1em; margin-bottom: 1em; } -.page__content .admonition .admonition, -.page__content .admonition details, -.page__content details .admonition, -.page__content details details { +.page__content .admonition .admonition { margin-bottom: 1em; margin-top: 1em; } -.page__content .admonition .admonition-title, -.page__content details summary { +.page__content .admonition-title { border: none; font-weight: 600; font-size: 0.9em; @@ -1097,8 +1073,7 @@ html.cs-dark img.invert { background-color: rgba(var(--admonition-rgb), 0.1); } -.page__content .admonition-title:before, -.page__content summary:before { +.page__content .admonition-title:before { content: ""; background-color: rgb(var(--admonition-rgb)); mask-image: var(--admonition-icon); @@ -1115,12 +1090,14 @@ html.cs-dark img.invert { justify-content: center; } -.page__content summary { - display: block; - /* Hides the marker */ +.page__content .admonition ::marker { + display: none; +} +.page__content .admonition summary { + list-style: none } -.page__content details>summary:after { +.page__content .admonition > summary:after { content: ""; background-color: rgb(var(--admonition-rgb)); mask-image: url('data:image/svg+xml;charset=utf-8,'); @@ -1139,52 +1116,30 @@ html.cs-dark img.invert { transition: transform var(--transition); } -.page__content details[open]>summary:after { +.page__content .admonition[open] > summary:after { transform: rotate(90deg); } -.page__content .admonition.tip, -.page__content details.tip { - --admonition-rgb: 0, 191, 165; +.page__content .admonition.tip { + --admonition-rgb: 0, 191, 150; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.warning, -.page__content details.warning { +.page__content .admonition.warning { --admonition-rgb: 255, 145, 0; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.error, -.page__content details.error { +.page__content .admonition.error { --admonition-rgb: 255, 23, 68; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.new, -.page__content details.new { +.page__content .admonition.new { --admonition-rgb: 233, 194, 0; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.example, -.page__content details.example { - --admonition-rgb: 168, 164, 159; - --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); - font-size: 1em; -} - -.page__content .admonition.example .admonition-title, -.page__content details.example summary { - font-size: 0.75em; -} - -.page__content .admonition.question, -.page__content details.question { - --admonition-rgb: 68, 64, 59; - --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); -} - /******* Search *******/ .search { @@ -1397,33 +1352,33 @@ html.cs-dark img.invert { color: inherit; } -.tabbed-content { +.tabbed-panels { width: 100%; } -.tabbed-block { +.tabbed-panel { display: none; padding-top: 0.75rem; } -.tabbed-set > input:first-child:checked ~ .tabbed-content > :first-child, -.tabbed-set > input:nth-child(2):checked ~ .tabbed-content > :nth-child(2), -.tabbed-set > input:nth-child(3):checked ~ .tabbed-content > :nth-child(3), -.tabbed-set > input:nth-child(4):checked ~ .tabbed-content > :nth-child(4), -.tabbed-set > input:nth-child(5):checked ~ .tabbed-content > :nth-child(5), -.tabbed-set > input:nth-child(6):checked ~ .tabbed-content > :nth-child(6), -.tabbed-set > input:nth-child(7):checked ~ .tabbed-content > :nth-child(7), -.tabbed-set > input:nth-child(8):checked ~ .tabbed-content > :nth-child(8), -.tabbed-set > input:nth-child(9):checked ~ .tabbed-content > :nth-child(9), -.tabbed-set > input:nth-child(10):checked ~ .tabbed-content > :nth-child(10), -.tabbed-set > input:nth-child(11):checked ~ .tabbed-content > :nth-child(11), -.tabbed-set > input:nth-child(12):checked ~ .tabbed-content > :nth-child(12), -.tabbed-set > input:nth-child(13):checked ~ .tabbed-content > :nth-child(13), -.tabbed-set > input:nth-child(14):checked ~ .tabbed-content > :nth-child(14), -.tabbed-set > input:nth-child(15):checked ~ .tabbed-content > :nth-child(15), -.tabbed-set > input:nth-child(16):checked ~ .tabbed-content > :nth-child(16), -.tabbed-set > input:nth-child(17):checked ~ .tabbed-content > :nth-child(17), -.tabbed-set > input:nth-child(18):checked ~ .tabbed-content > :nth-child(18), -.tabbed-set > input:nth-child(19):checked ~ .tabbed-content > :nth-child(19), -.tabbed-set > input:nth-child(20):checked ~ .tabbed-content > :nth-child(20) { +.tabbed-set > input:first-child:checked ~ .tabbed-panels > :first-child, +.tabbed-set > input:nth-child(2):checked ~ .tabbed-panels > :nth-child(2), +.tabbed-set > input:nth-child(3):checked ~ .tabbed-panels > :nth-child(3), +.tabbed-set > input:nth-child(4):checked ~ .tabbed-panels > :nth-child(4), +.tabbed-set > input:nth-child(5):checked ~ .tabbed-panels > :nth-child(5), +.tabbed-set > input:nth-child(6):checked ~ .tabbed-panels > :nth-child(6), +.tabbed-set > input:nth-child(7):checked ~ .tabbed-panels > :nth-child(7), +.tabbed-set > input:nth-child(8):checked ~ .tabbed-panels > :nth-child(8), +.tabbed-set > input:nth-child(9):checked ~ .tabbed-panels > :nth-child(9), +.tabbed-set > input:nth-child(10):checked ~ .tabbed-panels > :nth-child(10), +.tabbed-set > input:nth-child(11):checked ~ .tabbed-panels > :nth-child(11), +.tabbed-set > input:nth-child(12):checked ~ .tabbed-panels > :nth-child(12), +.tabbed-set > input:nth-child(13):checked ~ .tabbed-panels > :nth-child(13), +.tabbed-set > input:nth-child(14):checked ~ .tabbed-panels > :nth-child(14), +.tabbed-set > input:nth-child(15):checked ~ .tabbed-panels > :nth-child(15), +.tabbed-set > input:nth-child(16):checked ~ .tabbed-panels > :nth-child(16), +.tabbed-set > input:nth-child(17):checked ~ .tabbed-panels > :nth-child(17), +.tabbed-set > input:nth-child(18):checked ~ .tabbed-panels > :nth-child(18), +.tabbed-set > input:nth-child(19):checked ~ .tabbed-panels > :nth-child(19), +.tabbed-set > input:nth-child(20):checked ~ .tabbed-panels > :nth-child(20) { display: block; } @@ -1447,26 +1402,42 @@ html.cs-dark img.invert { /******* Stack *******/ -.stacked { +.stacked > p { display: flex; flex-direction: column; } @media (min-width: 48rem) { - .stacked { + .stacked > p { flex-direction: row; } - .stacked > * { + .stacked > p > * { transform: scale(0.95); transition: all var(--transition-slow); width: 100%; height: fit-content; } - .stacked :nth-child(2) { margin: 10% 0 0 -40%; } - .stacked :nth-child(3) { margin: 20% 0 0 -40%; } - .stacked :nth-child(4) { margin: 30% 0 0 -40%; } - .stacked :nth-child(5) { margin: 40% 0 0 -40%; } - .stacked > *:hover { + .stacked > p :nth-child(2) { margin: 10% 0 0 -40%; } + .stacked > p :nth-child(3) { margin: 20% 0 0 -40%; } + .stacked > p :nth-child(4) { margin: 30% 0 0 -40%; } + .stacked > p :nth-child(5) { margin: 40% 0 0 -40%; } + .stacked > p > *:hover { z-index: 20; transform: scale(1); } } + +/******* Example *******/ + +.page__content .example { + border-radius: 0 0.3rem 0.3rem 0; + border: 0; + box-shadow: 0 0 0 1px rgba(var(--rgb-gray), 0.1); + display: flow-root; + margin: var(--flow-space, 1.4em) 0; + padding: 0 1em; + page-break-inside: avoid; +} +.page__content .example > * { + margin-top: 1em; + margin-bottom: 1em; +} diff --git a/docs/content/api.md b/docs/content/api.md index 836b11d..d00539a 100644 --- a/docs/content/api.md +++ b/docs/content/api.md @@ -2,4 +2,5 @@ title: API --- -::: writeadoc.Docs +::: api writeadoc.Docs +::: diff --git a/docs/content/autodoc.md b/docs/content/autodoc.md index 3d60bc4..c893f7a 100644 --- a/docs/content/autodoc.md +++ b/docs/content/autodoc.md @@ -8,65 +8,102 @@ WriteADoc provides functionality for automatically generating documentation from ## Usage ```md -::: my_library.my_module.my_class_or_function +::: api my_library.my_module.my_class_or_function +::: ``` This works with classes, functions, and individual class methods and properties. -/// example | Function +## Example: Class ```md -::: jx.meta.extract_metadata +::: api jx.Catalog +::: ``` -::: jx.meta.extract_metadata -/// +::: api jx.Catalog +::: - +---- -/// example | Class +## Example: Function ```md -::: jx.Catalog +::: api jx.meta.extract_metadata +::: ``` -::: jx.Catalog -/// +::: api jx.meta.extract_metadata +::: -## Customizing What Is Documented +---- + + +## Options + +### Customizing What Is Documented By default, all members of a class whose names don't start with an underscore ("_") will be included. You can include one or more members that start with an underscore using the `include` option: ```md -::: jx.Catalog include=__call__,__html__ +::: api jx.Catalog +:include: __call__ __html__ +::: ``` You can also exclude some members with the `exclude` option: ```md -::: jx.Catalog exclude=get_data,to_dict include=__call__ +::: api jx.Catalog +:exclude: get_data to_dict +:include: __call__ +::: ``` -/// note +::: note As you can see, the options must be separated from each other by spaces. -/// +::: -## Changing the Starting Heading Level +### Showing only the class signature -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: +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 -

link

+

link

``` -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 tdset on em
ab
-``` - -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 ![The San Juan Mountains are beautiful!](/assets/images/san-juan-mountains.jpg "San Juan Mountains") ``` ![The San Juan Mountains are beautiful!](/assets/images/san-juan-mountains.jpg "San Juan Mountains") - -/// +::: ## 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 ![Lamp diagram](/assets/images/diagram-light.png){ .only-light } ![Lamp diagram](/assets/images/diagram-dark.png){ .only-dark } @@ -28,115 +25,84 @@ You can show different images for light and dark color schemes by using the `onl ![Lamp diagram](/assets/images/diagram-light.png){ .only-light } ![Lamp diagram](/assets/images/diagram-dark.png){ .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 ![Lamp diagram](/assets/images/diagram-light.png){ .invert } ``` ![Lamp diagram](/assets/images/diagram-light.png){ .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 ![Alt text](/assets/images/image.png){ .invert .left } ```md -
- +::: div columns ![Alt text](/assets/images/image.png){ .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) - -![Alt text](/assets/images/image.png){ .invert .right } - -```md -
- -![Alt text](/assets/images/image.png){ .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 ![Alt text](/assets/images/image.png){ .center } - ``` ![Alt text](/assets/images/image.png){ .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 ![Alt text](/assets/images/image.png) -
Image caption
-
+::: ``` -
-![Alt text](/assets/images/image.png){ .invert } -
Image caption
-
- -/// +::: figure | Image caption +![Alt text](/assets/images/image.png) +::: +:::: ## 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 [![Alt text](/assets/images/image.png "My title")](https://example.com/) ``` [![Alt text](/assets/images/image.png "My title"){ .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 ![Alt text](/assets/images/image.png "My title"){ width=100 } @@ -146,19 +112,16 @@ You can force an image to have a specific width and/or height by adding attribut ![Alt text](/assets/images/image.png "My title"){ .invert width=100 } ![Alt text](/assets/images/image.png "My title"){ .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 ![Alt text](/assets/images/opengraph.png){ loading=lazy } ``` ![Alt text](/assets/images/opengraph.png){ 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 ![Tux, the Linux mascot](/assets/images/tux.png) 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. -
- -![Nav A](/assets/images/nav-page-light.png){ .only-light .right } -![Nav A](/assets/images/nav-page-dark.png){ .only-dark .right } - +::: div columns ```python docs = Docs(__file__, pages=[ "overview/intro.md", @@ -61,7 +57,9 @@ docs = Docs(__file__, pages=[ ]) ``` -
+![Nav A](/assets/images/nav-page-light.png){ .only-light } +![Nav A](/assets/images/nav-page-dark.png){ .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. -
- -![Nav C](/assets/images/nav-section-light.png){ .only-light .right } -![Nav C](/assets/images/nav-section-dark.png){ .only-dark .right } - +::: div columns ```python docs = Docs(__file__, pages=[ { @@ -96,7 +90,9 @@ docs = Docs(__file__, pages=[ ]) ``` -
+![Nav C](/assets/images/nav-section-light.png){ .only-light } +![Nav C](/assets/images/nav-section-dark.png){ .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. -
- -![Nav C](/assets/images/nav-sectionpage-light.png){ .only-light .right } -![Nav C](/assets/images/nav-sectionpage-dark.png){ .only-dark .right } - +::: div columns ```python {hl_lines="4"} docs = Docs(__file__, pages=[ { @@ -132,7 +124,9 @@ docs = Docs(__file__, pages=[ ]) ``` -
+![Nav C](/assets/images/nav-sectionpage-light.png){ .only-light } +![Nav C](/assets/images/nav-sectionpage-dark.png){ .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. -
+::: div stacked only-light [![Home page](/assets/images/page-home-light.png)](/assets/images/page-home-light.png){ target="blank"} [![First page](/assets/images/page-index-light.png)](/assets/images/page-index-light.png){ target="blank"} -
+::: -
+::: div stacked only-dark [![Home page](/assets/images/page-home-dark.png)](/assets/images/page-home-dark.png){ target="blank"} [![First page](/assets/images/page-index-dark.png)](/assets/images/page-index-dark.png){ target="blank"} -
+::: ## 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 ![Version selector](/assets/images/version-selector-dark.png){ .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 %}

-
-{% endif -%} - -{%- if ds.params -%} -
- - - - {%- for param in ds.params %} - - - - - {%- endfor %} - -
ArgumentDescription
{{ param.name }}{{ param.description | markdown }}
-
-{% endif -%} - -{%- if ds.long_description -%} -
- {{ ds.long_description | markdown }} -
-{% endif -%} - -{% if ds.examples -%} -
-

Example:

- {% for ex in ds.examples -%} -
- {%- if ex.description %}{{ ex.description | markdown }}{% endif -%} - {%- if ex.snippet %} - {{ ex.snippet | markdown("python") }} - {% endif -%} -
- {% endfor -%} -
-{% endif -%} - -{%- if ds.returns or ds.many_returns -%} -
-

Returns:

- {%- if ds.returns -%} -

{{ ds.returns.description | markdown }}

- {%- elif ds.many_returns -%} -
    - {% for return in ds.many_returns %} -
  • - {{ return.return_name }}: - {{ return.description | markdown }} -
  • - {%- endfor %} -
- {% endif -%} -
-{% endif -%} - -{%- if ds.raises -%} -
-

Raises:

-
    - {% for raises in ds.raises -%} -
  • {{ raises.description | markdown }}
  • - {% endfor -%} -
-
-{% endif -%} - -{%- if ds.attrs-%} -
- {% for attr in ds.attrs -%} - - {% endfor %} -
-{% endif -%} - -{%- if ds.properties -%} -
- {% for attr in ds.properties -%} - - {%- endfor %} -
-{% endif -%} - -{%- if ds.methods -%} -
-{% for method in ds.methods -%} - -{%- endfor %} -
-{% endif -%} \ No newline at end of file diff --git a/docs/views/autodoc.md.jinja b/docs/views/autodoc.md.jinja new file mode 100644 index 0000000..d39f23d --- /dev/null +++ b/docs/views/autodoc.md.jinja @@ -0,0 +1,107 @@ +{# import "autodoc.md.jinja" as Autodoc #} +{# def ds, level=2 #} + +{%- if ds.symbol or ds.name %} +{{ "#" * level }} `{{ ds.symbol }}`{ .autodoc-symbol .autodoc-symbol-{{ ds.symbol }} } `{{ ds.name }}`{ .autodoc-name .autodoc-name-{{ ds.symbol }} } +{%- if ds.label %} `{{ ds.label }}`{ .autodoc-label .autodoc-label-{{ ds.label }} }{% endif %} +{% endif -%} + +{%- if ds.short_description %} +:::: div autodoc-short-description +{{ ds.short_description | safe }} +:::: +{% endif -%} + +{%- if ds.signature %} +````python +{{ ds.signature | safe }} +```` +{% endif -%} + +{%- if ds.bases -%} +:::: div autodoc-bases +Bases: {% for base in ds.bases %} `{{ base }}`{% if not loop.last %}, {% endif %} +{%- endfor %} +:::: +{% endif -%} + +{%- if ds.params %} +:::: div autodoc-table autodoc-arguments + +Argument | Description +-------- | -------- +{%- for param in ds.params %} +`{{ param.name }}` | {{ param.description | replace('\n', '
') | safe }} +{%- endfor %} + +:::: +{% endif -%} + +{%- if ds.long_description %} +:::: div autodoc-long-description +{{ ds.long_description | safe }} +:::: +{% endif %} +{%- if ds.examples %} +:::: div autodoc-examples +**Example:** + +{% for ex in ds.examples -%} +::: div +{%- if ex.description %}{{ ex.description | safe }} +{% endif -%} +{%- if ex.snippet %} +````python +{{ ex.snippet | safe }} +```` +{% endif %} +::: +{% endfor %} +:::: +{% endif -%} + +{%- if ds.returns or ds.many_returns %} +:::: div autodoc-returns +**Returns:** +{%- if ds.returns %} +{{ ds.returns.description | safe }} +{%- elif ds.many_returns -%} +{%- for return in ds.many_returns %} +- **{{ return.return_name }}**: {{ return.description | safe }} +{%- endfor %} +{% endif %} +:::: +{% endif -%} + +{%- if ds.raises %} +:::: div autodoc-raises +**Raises:** +{% for raises in ds.raises -%} +- {{ raises.description | safe }} +{% endfor %} +:::: +{% endif -%} + +{%- if ds.attrs %} +::::: div autodoc-attrs +{% for attr in ds.attrs %} + +{% endfor %} +::::: +{% endif -%} + +{%- if ds.properties %} +::::: div autodoc-properties +{% for attr in ds.properties %} + +{%- endfor %} +::::: +{% endif -%} + +{%- if ds.methods %} +::::: div autodoc-methods +{% for method in ds.methods %} + +{%- endfor %} +::::: +{% endif -%} \ No newline at end of file diff --git a/docs/views/page_toc.jinja b/docs/views/page_toc.jinja index 7dfe12f..dfa37e7 100644 --- a/docs/views/page_toc.jinja +++ b/docs/views/page_toc.jinja @@ -1,9 +1,7 @@ {% macro render_toc(items) %}
    - {% for item in items -%} -
  • {{ item.html|safe }} - {%- if item.children %}{{ render_toc(item.children) }}{% endif -%} -
  • + {% for level, id, text in items -%} +
  • {{ text|safe }}
  • {% endfor %}
{% endmacro %} diff --git a/pyproject.toml b/pyproject.toml index 01a59ee..cb86ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writeadoc" -version = "0.9.1" +version = "0.10.0" description = "Focus on your content and let WriteADoc take care of the rest" authors = [ {name = "Juan Pablo Scaletti", email = "juanpablo@jpscaletti.com"}, @@ -13,10 +13,10 @@ dependencies = [ "hecto>=2.0.1", "jinja2>=3.1.6", "jx>=0.3.0", - "markdown>=3.8.2", + "mistune>=3.2.0", "pygments>=2.19.2", - "pymdown-extensions>=10.16", "strictyaml>=1.7.3", + "ty>=0.0.1a15", "watchdog>=6.0.0", ] @@ -27,6 +27,7 @@ GitHub = "https://github.com/jpsca/writeadoc" [dependency-groups] dev = [ "ipdb>=0.13.13", + "pytest-cov>=7.0.0", "ruff>=0.12.4", "tox-uv", "ty>=0.0.1a15", @@ -69,6 +70,7 @@ exclude_lines = [ ] omit = [ "src/writeadoc/blueprint/**", + "src/writeadoc/cli.py", ] [tool.coverage.html] diff --git a/src/writeadoc/__init__.py b/src/writeadoc/__init__.py index 9d561a5..1b4c258 100644 --- a/src/writeadoc/__init__.py +++ b/src/writeadoc/__init__.py @@ -1,7 +1,4 @@ from .exceptions import * # noqa from .main import Docs # noqa from .types import * # noqa -from .utils import ( - DEFAULT_MD_EXTENSIONS, # noqa - DEFAULT_MD_CONFIG, # noqa -) + diff --git a/src/writeadoc/autodoc.py b/src/writeadoc/autodoc.py index 295f868..90e43c3 100644 --- a/src/writeadoc/autodoc.py +++ b/src/writeadoc/autodoc.py @@ -1,4 +1,5 @@ import inspect +import re import typing as t from dataclasses import dataclass, field from importlib import import_module @@ -13,6 +14,15 @@ ) +RX_AUTODOC = re.compile( + r"^:::\s*api(\s*\|)?\s+(?P[^\n]+)(?:\n|$)" + r"(?P(?:\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)" + r"^:::", + re.MULTILINE, +) +RX_FENCED = re.compile(r"\n```+.*?\n[\s\S]*?\n```+\n", re.MULTILINE) + + @dataclass(slots=True) class AttrDocstring: symbol: str = "" @@ -59,240 +69,297 @@ class Docstring: methods: list["Docstring"] = field(default_factory=list) -class Autodoc: - - def __call__( - self, - name: str, - *, - show_name: bool = True, - show_members: bool = True, - include: tuple[str, ...] = (), - exclude: tuple[str, ...] = (), - ) -> Docstring: - module_name, obj_name = name.rsplit(".", 1) - attr = None - if ":" in obj_name: - obj_name, attr = obj_name.split(":", 1) - module = import_module(module_name) - assert module - obj = getattr(module, obj_name, None) - if not obj: - raise ValueError(f"Object {obj_name} not found in module {module_name}") - if attr: - obj = getattr(obj, attr, None) - if not obj: - raise ValueError(f"Attribute {attr} not found in object {obj_name}") +def render_autodoc(source: str, *, render: t.Callable) -> str: + blocks = find_autodocs_blocks(source) + for block in reversed(blocks): + level = int(block["options"].pop("level", 2)) + ds = autodoc(block["name"], **block["options"]) + frag = render(ds=ds, level=level) + frag = str(frag).strip() + start = block["start"] + end = block["end"] + source = f"{source[:start]}{frag}{source[end:]}" - return self.autodoc_obj( - obj, - show_name=show_name, - show_members=show_members, - include=include, - exclude=exclude - ) + return source.strip() - def autodoc_obj( - self, - obj: t.Any, - *, - show_name: bool = True, - show_members: bool = True, - include: tuple[str, ...] = (), - exclude: tuple[str, ...] = (), - ) -> Docstring: - if inspect.isclass(obj): - ds = self.autodoc_class( - obj, - show_name=show_name, - show_members=show_members, - include=include, - exclude=exclude, - ) - elif inspect.isfunction(obj) or inspect.ismethod(obj): - ds = self.autodoc_function(obj, show_name=show_name) - else: - ds = Docstring() - return ds - - def autodoc_class( - self, - obj: t.Any, - *, - symbol: str = "class", - show_name: bool = True, - show_members: bool = True, - include: tuple[str, ...] = (), - exclude: tuple[str, ...] = (), - ) -> Docstring: - init = getattr(obj, "__init__", None) - obj_name = obj.__name__ - ds = parse(obj.__doc__ or init.__doc__ or "") - - description = (ds.description or "").strip() - short_description, long_description = self.split_description(description) - - params = [] - attrs = [] - properties = [] - methods = [] - - for param in ds.params: - doc = self.autodoc_attr(param) - if param.args[0] == "param": - params.append(doc) - elif param.args[0] == "attribute": - attrs.append(doc) - - exclude_all = "*" in exclude - if show_members: - for name, value in inspect.getmembers(obj): - if name.startswith("_") and name not in include: - continue - if (exclude_all or name in exclude) and name not in include: - continue - if inspect.isfunction(value): - methods.append(self.autodoc_function(value)) - continue - if isinstance(value, property): - properties.append(self.autodoc_property(name, value)) - continue - - if ds.deprecation: - ds.deprecation.description = (ds.deprecation.description or "").strip() - if ds.returns: - ds.returns.description = (ds.returns.description or "").strip() - for meta in ds.raises: - meta.description = (meta.description or "").strip() - for meta in ds.many_returns: - meta.description = (meta.description or "").strip() - for meta in ds.examples: - meta.snippet = (meta.snippet or "").strip() - meta.description = (meta.description or "").strip() - - return Docstring( - symbol=symbol if show_name else "", - name=obj_name if show_name else "", - signature=self.get_signature(obj_name, init), - params=params, - short_description=short_description, - long_description=long_description, - description=description, - deprecation=ds.deprecation, - returns=ds.returns, - raises=ds.raises, - examples=ds.examples, - many_returns=ds.many_returns, - bases=[base.__name__ for base in obj.__bases__ if base.__name__ != "object"], - attrs=attrs, - properties=properties, - methods=methods, - ) - def autodoc_function( - self, - obj: t.Any, - *, - symbol: str = "", - show_name: bool = True, - ) -> Docstring: - obj_name = obj.__name__ - ds = parse(obj.__doc__ or "") - - description = (ds.description or "").strip() - short_description, long_description = self.split_description(description) - params = [self.autodoc_attr(param) for param in ds.params] - - if ds.deprecation: - ds.deprecation.description = (ds.deprecation.description or "").strip() - if ds.returns: - ds.returns.description = (ds.returns.description or "").strip() - for meta in ds.raises: - meta.description = (meta.description or "").strip() - for meta in ds.many_returns: - meta.description = (meta.description or "").strip() - for meta in ds.examples: - meta.snippet = (meta.snippet or "").strip() - meta.description = (meta.description or "").strip() - - if not symbol: - if inspect.ismethod(obj): - symbol = "method" - elif inspect.iscoroutinefunction(obj): - symbol = "async function" - else: - symbol = "function" - return Docstring( - symbol=symbol if show_name else "", - name=obj_name if show_name else "", - signature=self.get_signature(obj_name, obj), - params=params, - short_description=short_description, - long_description=long_description, - description=description, - deprecation=ds.deprecation, - returns=ds.returns, - raises=ds.raises, - examples=ds.examples, - many_returns=ds.many_returns, - ) +def find_autodocs_blocks(source: str) -> list[dict[str, t.Any]]: + # Find all ::: api ... blocks + api_blocks = list(RX_AUTODOC.finditer(source)) - def autodoc_property( - self, name: str, obj: t.Any, *, symbol: str = "attr" - ) -> Docstring: - ds = parse(obj.__doc__ or "") - description = (ds.description or "").strip() - short_description, long_description = self.split_description(description) - - return Docstring( - name=name, - symbol=symbol, - label="property", - short_description=short_description, - long_description=long_description, - description=description, - deprecation=ds.deprecation, - returns=ds.returns, - raises=ds.raises, - examples=ds.examples, - many_returns=ds.many_returns, - ) + # Find all code fence regions + code_fences = [m.span() for m in RX_FENCED.finditer(source)] - def autodoc_attr( - self, attr: DocstringParam, *, symbol: str = "attr" - ) -> AttrDocstring: - if attr.type_name: - name = f"{attr.arg_name}: {attr.type_name}" - else: - name = attr.arg_name - - description = (attr.description or "").strip() - short_description, long_description = self.split_description(description) - - return AttrDocstring( - symbol=symbol, - name=name, - label="attribute", - short_description=short_description, - long_description=long_description, - description=description, + def is_inside_code_fence(start, code_fences): + return any( + fence_start <= start < fence_end for fence_start, fence_end in code_fences ) - def get_signature(self, obj_name: str, obj: t.Any) -> str: - sig = inspect.signature(obj) - str_sig = ( - format_signature(sig, max_width=5).replace(" self,\n", "").replace("(self)", "()") + return [ + { + "name": m.group("name").strip(), + "options": parse_options(m), + "start": m.start(), + "end": m.end(), + } + for m in api_blocks + if not is_inside_code_fence(m.start(), code_fences) + ] + + +def parse_options(m: re.Match[str]) -> dict[str, str]: + text = m.group("options") + if not text.strip(): + return {} + + options = [] + for line in re.split(r"\n+", text): + line = line.strip()[1:] + if not line: + continue + + i = line.find(":") + k = line[:i] + v = line[i + 1 :].strip() + + if v.lower() == "true": + v = True + elif v.lower() == "false": + v = False + elif v.isdigit(): + v = int(v) + options.append((k, v)) + + return dict(options) + + +def autodoc( + name: str, + *, + show_members: bool = True, + include: tuple[str, ...] = (), + exclude: tuple[str, ...] = (), +) -> Docstring: + module_name, obj_name = name.rsplit(".", 1) + attr = None + if ":" in obj_name: + obj_name, attr = obj_name.split(":", 1) + module = import_module(module_name) + assert module + obj = getattr(module, obj_name, None) + if not obj: + raise ValueError(f"Object {obj_name} not found in module {module_name}") + if attr: + obj = getattr(obj, attr, None) + if not obj: + raise ValueError(f"Attribute {attr} not found in object {obj_name}") + + return autodoc_obj( + obj, + show_members=show_members, + include=include, + exclude=exclude, + ) + + +def autodoc_obj( + obj: t.Any, + *, + show_members: bool = True, + include: tuple[str, ...] = (), + exclude: tuple[str, ...] = (), +) -> Docstring: + if inspect.isclass(obj): + ds = autodoc_class( + obj, + show_members=show_members, + include=include, + exclude=exclude, ) - return f"{obj_name}{str_sig}" - - def split_description(self, description: str) -> tuple[str, str]: - if "\n\n" not in description: - return description, "" - head, rest = description.split("\n\n", 1) - return head, rest - - -def format_signature(sig, *, max_width=None): + elif inspect.isfunction(obj) or inspect.ismethod(obj): + ds = autodoc_function(obj) + else: + ds = Docstring() + return ds + + +def autodoc_class( + obj: t.Any, + *, + symbol: str = "class", + show_members: bool = True, + include: tuple[str, ...] = (), + exclude: tuple[str, ...] = (), +) -> Docstring: + init = getattr(obj, "__init__", None) + obj_name = obj.__name__ + ds = parse(obj.__doc__ or init.__doc__ or "") + + description = (ds.description or "").strip() + short_description, long_description = split_description(description) + + params = [] + attrs = [] + properties = [] + methods = [] + + for param in ds.params: + doc = autodoc_attr(param) + if param.args[0] == "param": + params.append(doc) + elif param.args[0] == "attribute": + attrs.append(doc) + + exclude_all = "*" in exclude + if show_members: + for name, value in inspect.getmembers(obj): + if name.startswith("_") and name not in include: + continue + if (exclude_all or name in exclude) and name not in include: + continue + if inspect.isfunction(value): + methods.append(autodoc_function(value)) + continue + if isinstance(value, property): + properties.append(autodoc_property(name, value)) + continue + + if ds.deprecation: + ds.deprecation.description = (ds.deprecation.description or "").strip() + if ds.returns: + ds.returns.description = (ds.returns.description or "").strip() + for meta in ds.raises: + meta.description = (meta.description or "").strip() + for meta in ds.many_returns: + meta.description = (meta.description or "").strip() + for meta in ds.examples: + meta.snippet = (meta.snippet or "").strip() + meta.description = (meta.description or "").strip() + + return Docstring( + symbol=symbol, + name=obj_name, + signature=get_signature(obj_name, init), + params=params, + short_description=short_description, + long_description=long_description, + description=description, + deprecation=ds.deprecation, + returns=ds.returns, + raises=ds.raises, + examples=ds.examples, + many_returns=ds.many_returns, + bases=[base.__name__ for base in obj.__bases__ if base.__name__ != "object"], + attrs=attrs, + properties=properties, + methods=methods, + ) + + +def autodoc_function( + obj: t.Any, + *, + symbol: str = "", +) -> Docstring: + obj_name = obj.__name__ + ds = parse(obj.__doc__ or "") + + description = (ds.description or "").strip() + short_description, long_description = split_description(description) + params = [autodoc_attr(param) for param in ds.params] + + if ds.deprecation: + ds.deprecation.description = (ds.deprecation.description or "").strip() + if ds.returns: + ds.returns.description = (ds.returns.description or "").strip() + for meta in ds.raises: + meta.description = (meta.description or "").strip() + for meta in ds.many_returns: + meta.description = (meta.description or "").strip() + for meta in ds.examples: + meta.snippet = (meta.snippet or "").strip() + meta.description = (meta.description or "").strip() + + if not symbol: + if inspect.ismethod(obj): + symbol = "method" + elif inspect.iscoroutinefunction(obj): + symbol = "async function" + else: + symbol = "function" + return Docstring( + symbol=symbol, + name=obj_name, + signature=get_signature(obj_name, obj), + params=params, + short_description=short_description, + long_description=long_description, + description=description, + deprecation=ds.deprecation, + returns=ds.returns, + raises=ds.raises, + examples=ds.examples, + many_returns=ds.many_returns, + ) + + +def autodoc_property(name: str, obj: t.Any, *, symbol: str = "attr") -> Docstring: + ds = parse(obj.__doc__ or "") + description = (ds.description or "").strip() + short_description, long_description = split_description(description) + + return Docstring( + name=name, + symbol=symbol, + label="property", + short_description=short_description, + long_description=long_description, + description=description, + deprecation=ds.deprecation, + returns=ds.returns, + raises=ds.raises, + examples=ds.examples, + many_returns=ds.many_returns, + ) + + +def autodoc_attr(attr: DocstringParam, *, symbol: str = "attr") -> AttrDocstring: + if attr.type_name: + name = f"{attr.arg_name}: {attr.type_name}" + else: + name = attr.arg_name + + description = (attr.description or "").strip() + short_description, long_description = split_description(description) + + return AttrDocstring( + symbol=symbol, + name=name, + label="attribute", + short_description=short_description, + long_description=long_description, + description=description, + ) + + +RX_SELF = re.compile(r"\bself\b,?\s*") + + +def get_signature(obj_name: str, obj: t.Any, max_width: int = 10) -> str: + sig = inspect.signature(obj) + str_sig = format_signature(sig, max_width=max_width) + str_sig = RX_SELF.sub("", str_sig) + return f"{obj_name}{str_sig}" + + +def split_description(description: str) -> tuple[str, str]: + if "\n\n" not in description: + return description, "" + head, rest = description.split("\n\n", 1) + return head, rest + + +def format_signature(sig, *, max_width: int = -1) -> str: """Create a string representation of the Signature object. If *max_width* integer is passed, signature will try to fit into the *max_width*. @@ -334,7 +401,7 @@ def format_signature(sig, *, max_width=None): result.append("/") rendered = "({})".format(", ".join(result)) - if max_width is not None and len(rendered) > max_width: + if max_width > 0 and len(rendered) > max_width: rendered = "(\n {}\n)".format(",\n ".join(result)) if sig.return_annotation is not inspect._empty: diff --git a/src/writeadoc/blueprint/assets/css/base.css b/src/writeadoc/blueprint/assets/css/base.css index e29beda..062f975 100644 --- a/src/writeadoc/blueprint/assets/css/base.css +++ b/src/writeadoc/blueprint/assets/css/base.css @@ -225,6 +225,9 @@ ol, ul { :is(ol,ul) :is(ol,ul) ul { list-style: square; } +li [type="checkbox"] { + margin-right: 0.5em; +} ul { list-style: disc @@ -1027,8 +1030,7 @@ html.cs-dark img.invert { /******* Page - Admonitions *******/ -.page__content .admonition, -.page__content details { +.page__content .admonition { --admonition-rgb: 43, 127, 255; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); @@ -1043,50 +1045,24 @@ html.cs-dark img.invert { page-break-inside: avoid; } -@media screen and (min-width: 60em) { - .page__content .admonition.left:only-child, - .page__content details.left:only-child, - .page__content .admonition.right:only-child, - .page__content details.right:only-child { - margin-top: 0; - } - .page__content .admonition.left, - .page__content details.left { - float: left; - margin: 1em 1em 1em 0; - width: 40%; - } - .page__content .admonition.right, - .page__content details.right { - float: right; - margin: 1em 0 1em 1em; - width: 40%; - } -} @media print { - .page__content .admonition, - .page__content details { + .page__content .admonition { box-shadow: none; } } -.page__content .admonition>*, -.page__content details>* { +.page__content .admonition > * { margin-top: 1em; margin-bottom: 1em; } -.page__content .admonition .admonition, -.page__content .admonition details, -.page__content details .admonition, -.page__content details details { +.page__content .admonition .admonition { margin-bottom: 1em; margin-top: 1em; } -.page__content .admonition .admonition-title, -.page__content details summary { +.page__content .admonition-title { border: none; font-weight: 600; font-size: 0.9em; @@ -1097,8 +1073,7 @@ html.cs-dark img.invert { background-color: rgba(var(--admonition-rgb), 0.1); } -.page__content .admonition-title:before, -.page__content summary:before { +.page__content .admonition-title:before { content: ""; background-color: rgb(var(--admonition-rgb)); mask-image: var(--admonition-icon); @@ -1115,12 +1090,14 @@ html.cs-dark img.invert { justify-content: center; } -.page__content summary { - display: block; - /* Hides the marker */ +.page__content .admonition ::marker { + display: none; +} +.page__content .admonition summary { + list-style: none } -.page__content details>summary:after { +.page__content .admonition > summary:after { content: ""; background-color: rgb(var(--admonition-rgb)); mask-image: url('data:image/svg+xml;charset=utf-8,'); @@ -1139,52 +1116,30 @@ html.cs-dark img.invert { transition: transform var(--transition); } -.page__content details[open]>summary:after { +.page__content .admonition[open] > summary:after { transform: rotate(90deg); } -.page__content .admonition.tip, -.page__content details.tip { - --admonition-rgb: 0, 191, 165; +.page__content .admonition.tip { + --admonition-rgb: 0, 191, 150; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.warning, -.page__content details.warning { +.page__content .admonition.warning { --admonition-rgb: 255, 145, 0; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.error, -.page__content details.error { +.page__content .admonition.error { --admonition-rgb: 255, 23, 68; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.new, -.page__content details.new { +.page__content .admonition.new { --admonition-rgb: 233, 194, 0; --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); } -.page__content .admonition.example, -.page__content details.example { - --admonition-rgb: 168, 164, 159; - --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); - font-size: 1em; -} - -.page__content .admonition.example .admonition-title, -.page__content details.example summary { - font-size: 0.75em; -} - -.page__content .admonition.question, -.page__content details.question { - --admonition-rgb: 68, 64, 59; - --admonition-icon: url('data:image/svg+xml;charset=utf-8,'); -} - /******* Search *******/ .search { @@ -1397,33 +1352,33 @@ html.cs-dark img.invert { color: inherit; } -.tabbed-content { +.tabbed-panels { width: 100%; } -.tabbed-block { +.tabbed-panel { display: none; padding-top: 0.75rem; } -.tabbed-set > input:first-child:checked ~ .tabbed-content > :first-child, -.tabbed-set > input:nth-child(2):checked ~ .tabbed-content > :nth-child(2), -.tabbed-set > input:nth-child(3):checked ~ .tabbed-content > :nth-child(3), -.tabbed-set > input:nth-child(4):checked ~ .tabbed-content > :nth-child(4), -.tabbed-set > input:nth-child(5):checked ~ .tabbed-content > :nth-child(5), -.tabbed-set > input:nth-child(6):checked ~ .tabbed-content > :nth-child(6), -.tabbed-set > input:nth-child(7):checked ~ .tabbed-content > :nth-child(7), -.tabbed-set > input:nth-child(8):checked ~ .tabbed-content > :nth-child(8), -.tabbed-set > input:nth-child(9):checked ~ .tabbed-content > :nth-child(9), -.tabbed-set > input:nth-child(10):checked ~ .tabbed-content > :nth-child(10), -.tabbed-set > input:nth-child(11):checked ~ .tabbed-content > :nth-child(11), -.tabbed-set > input:nth-child(12):checked ~ .tabbed-content > :nth-child(12), -.tabbed-set > input:nth-child(13):checked ~ .tabbed-content > :nth-child(13), -.tabbed-set > input:nth-child(14):checked ~ .tabbed-content > :nth-child(14), -.tabbed-set > input:nth-child(15):checked ~ .tabbed-content > :nth-child(15), -.tabbed-set > input:nth-child(16):checked ~ .tabbed-content > :nth-child(16), -.tabbed-set > input:nth-child(17):checked ~ .tabbed-content > :nth-child(17), -.tabbed-set > input:nth-child(18):checked ~ .tabbed-content > :nth-child(18), -.tabbed-set > input:nth-child(19):checked ~ .tabbed-content > :nth-child(19), -.tabbed-set > input:nth-child(20):checked ~ .tabbed-content > :nth-child(20) { +.tabbed-set > input:first-child:checked ~ .tabbed-panels > :first-child, +.tabbed-set > input:nth-child(2):checked ~ .tabbed-panels > :nth-child(2), +.tabbed-set > input:nth-child(3):checked ~ .tabbed-panels > :nth-child(3), +.tabbed-set > input:nth-child(4):checked ~ .tabbed-panels > :nth-child(4), +.tabbed-set > input:nth-child(5):checked ~ .tabbed-panels > :nth-child(5), +.tabbed-set > input:nth-child(6):checked ~ .tabbed-panels > :nth-child(6), +.tabbed-set > input:nth-child(7):checked ~ .tabbed-panels > :nth-child(7), +.tabbed-set > input:nth-child(8):checked ~ .tabbed-panels > :nth-child(8), +.tabbed-set > input:nth-child(9):checked ~ .tabbed-panels > :nth-child(9), +.tabbed-set > input:nth-child(10):checked ~ .tabbed-panels > :nth-child(10), +.tabbed-set > input:nth-child(11):checked ~ .tabbed-panels > :nth-child(11), +.tabbed-set > input:nth-child(12):checked ~ .tabbed-panels > :nth-child(12), +.tabbed-set > input:nth-child(13):checked ~ .tabbed-panels > :nth-child(13), +.tabbed-set > input:nth-child(14):checked ~ .tabbed-panels > :nth-child(14), +.tabbed-set > input:nth-child(15):checked ~ .tabbed-panels > :nth-child(15), +.tabbed-set > input:nth-child(16):checked ~ .tabbed-panels > :nth-child(16), +.tabbed-set > input:nth-child(17):checked ~ .tabbed-panels > :nth-child(17), +.tabbed-set > input:nth-child(18):checked ~ .tabbed-panels > :nth-child(18), +.tabbed-set > input:nth-child(19):checked ~ .tabbed-panels > :nth-child(19), +.tabbed-set > input:nth-child(20):checked ~ .tabbed-panels > :nth-child(20) { display: block; } @@ -1447,26 +1402,42 @@ html.cs-dark img.invert { /******* Stack *******/ -.stacked { +.stacked > p { display: flex; flex-direction: column; } @media (min-width: 48rem) { - .stacked { + .stacked > p { flex-direction: row; } - .stacked > * { + .stacked > p > * { transform: scale(0.95); transition: all var(--transition-slow); width: 100%; height: fit-content; } - .stacked :nth-child(2) { margin: 10% 0 0 -40%; } - .stacked :nth-child(3) { margin: 20% 0 0 -40%; } - .stacked :nth-child(4) { margin: 30% 0 0 -40%; } - .stacked :nth-child(5) { margin: 40% 0 0 -40%; } - .stacked > *:hover { + .stacked > p :nth-child(2) { margin: 10% 0 0 -40%; } + .stacked > p :nth-child(3) { margin: 20% 0 0 -40%; } + .stacked > p :nth-child(4) { margin: 30% 0 0 -40%; } + .stacked > p :nth-child(5) { margin: 40% 0 0 -40%; } + .stacked > p > *:hover { z-index: 20; transform: scale(1); } } + +/******* Example *******/ + +.page__content .example { + border-radius: 0 0.3rem 0.3rem 0; + border: 0; + box-shadow: 0 0 0 1px rgba(var(--rgb-gray), 0.1); + display: flow-root; + margin: var(--flow-space, 1.4em) 0; + padding: 0 1em; + page-break-inside: avoid; +} +.page__content .example > * { + margin-top: 1em; + margin-bottom: 1em; +} diff --git a/src/writeadoc/blueprint/views/autodoc.jinja b/src/writeadoc/blueprint/views/autodoc.jinja index cbd78cd..e69de29 100644 --- a/src/writeadoc/blueprint/views/autodoc.jinja +++ b/src/writeadoc/blueprint/views/autodoc.jinja @@ -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 %}

-
-{% endif -%} - -{%- if ds.params -%} -
- - - - {%- for param in ds.params %} - - - - - {%- endfor %} - -
ArgumentDescription
{{ param.name }}{{ param.description | markdown }}
-
-{% endif -%} - -{%- if ds.long_description -%} -
- {{ ds.long_description | markdown }} -
-{% endif -%} - -{% if ds.examples -%} -
-

Example:

- {% for ex in ds.examples -%} -
- {%- if ex.description %}{{ ex.description | markdown }}{% endif -%} - {%- if ex.snippet %} - {{ ex.snippet | markdown("python") }} - {% endif -%} -
- {% endfor -%} -
-{% endif -%} - -{%- if ds.returns or ds.many_returns -%} -
-

Returns:

- {%- if ds.returns -%} -

{{ ds.returns.description | markdown }}

- {%- elif ds.many_returns -%} -
    - {% for return in ds.many_returns %} -
  • - {{ return.return_name }}: - {{ return.description | markdown }} -
  • - {%- endfor %} -
- {% endif -%} -
-{% endif -%} - -{%- if ds.raises -%} -
-

Raises:

-
    - {% for raises in ds.raises -%} -
  • {{ raises.description | markdown }}
  • - {% endfor -%} -
-
-{% endif -%} - -{%- if ds.attrs-%} -
- {% for attr in ds.attrs -%} - - {% endfor %} -
-{% endif -%} - -{%- if ds.properties -%} -
- {% for attr in ds.properties -%} - - {%- endfor %} -
-{% endif -%} - -{%- if ds.methods -%} -
-{% for method in ds.methods -%} - -{%- endfor %} -
-{% endif -%} \ No newline at end of file diff --git a/src/writeadoc/blueprint/views/page_toc.jinja b/src/writeadoc/blueprint/views/page_toc.jinja index 7dfe12f..dfa37e7 100644 --- a/src/writeadoc/blueprint/views/page_toc.jinja +++ b/src/writeadoc/blueprint/views/page_toc.jinja @@ -1,9 +1,7 @@ {% macro render_toc(items) %}
    - {% for item in items -%} -
  • {{ item.html|safe }} - {%- if item.children %}{{ render_toc(item.children) }}{% endif -%} -
  • + {% for level, id, text in items -%} +
  • {{ text|safe }}
  • {% 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" - - 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'

{text}

\n' + + +def render_admonition_content(self: t.Any, text: str) -> str: + return text diff --git a/src/writeadoc/md/attrs.py b/src/writeadoc/md/attrs.py new file mode 100644 index 0000000..5dfdf96 --- /dev/null +++ b/src/writeadoc/md/attrs.py @@ -0,0 +1,103 @@ +import re +import typing as t + +from mistune import BlockParser, BlockState, InlineParser, InlineState +from mistune.markdown import Markdown + + +RE_ATTRS = r"\{\s*([^\}]+)\s*\}" + + +def _handle_double_quote(s, tk): + k, v = tk.split("=", 1) + return k, v.strip('"') + + +def _handle_single_quote(s, tk): + k, v = tk.split("=", 1) + return k, v.strip("'") + + +def _handle_key_value(s, tk): + return tk.split("=", 1) + + +def _handle_word(s, tk): + if tk.startswith("."): + return ".", tk[1:] + if tk.startswith("#"): + return "id", tk[1:] + return tk, True + + +_scanner = re.Scanner( # type: ignore + [ + (r'[^ =}]+=".*?"', _handle_double_quote), + (r"[^ =}]+='.*?'", _handle_single_quote), + (r"[^ =}]+=[^ =}]+", _handle_key_value), + (r"[^ =}]+", _handle_word), + (r" ", None), + ] +) + + +def parse_attrs(attrs_str: str) -> dict[str, t.Any]: + """Parse attribute list and return a list of attribute tuples. + """ + attrs_str = attrs_str.strip("{}").strip() + _attrs, _remainder = _scanner.scan(attrs_str) + + attrs = {} + classes = {} + for k, v in _attrs: + if k == ".": + classes[v] = 1 + elif k == "#": + attrs["id"] = v + else: + attrs[k] = v + + if classes: + str_classes = " ".join(classes.keys()) + if "class" in attrs: + attrs["class"] += " " + str_classes + else: + attrs["class"] = str_classes + + return dict(attrs) + + +def attach_inline_attrs(inline: InlineParser, m: re.Match, state: InlineState) -> int: + attrs_str = m.groupdict().get("inline_attrs") + if attrs_str and state.tokens: + prev = state.tokens[-1] + attrs = parse_attrs(attrs_str) + prev.setdefault("attrs", {}) + prev["attrs"].update(attrs) + return m.end() + + +def attach_block_attrs(block: BlockParser, m: re.Match, state: BlockState) -> int: + attrs_str = m.groupdict().get("block_attrs") + if attrs_str and state.tokens: + prev = state.tokens[-1] + attrs = parse_attrs(attrs_str) + prev.setdefault("attrs", {}) + prev["attrs"].update(attrs) + return m.end() + + +def inline_attrs(md: Markdown) -> None: + md.inline.register( + "inline_attrs", + RE_ATTRS, + attach_inline_attrs, + before="link" + ) + +def block_attrs(md: Markdown) -> None: + md.block.register( + "block_attrs", + rf"^\s*{RE_ATTRS}\s$", + attach_block_attrs, + ) diff --git a/src/writeadoc/md/block_directive.py b/src/writeadoc/md/block_directive.py new file mode 100644 index 0000000..dad90da --- /dev/null +++ b/src/writeadoc/md/block_directive.py @@ -0,0 +1,64 @@ +import re +import typing as t + +from mistune.directives import DirectivePlugin, FencedDirective + + +if t.TYPE_CHECKING: + from mistune.block_parser import BlockParser + from mistune.core import BlockState + + +_directive_re = re.compile( + r"\s*(?P[a-zA-Z0-9_-]+)(\s*\|)? *(?P[^\n]*)(?:\n|$)" + r"(?P<options>(?:\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)" + r"\n*(?P<text>(?:[^\n]*\n+)*)" +) + + +class BlockDirective(FencedDirective): + """A **fenced** style of directive that uses the more popular `:::` syntax + and removes the braces around the directive type. + + The syntax looks like: + + ```markdown + ::: directive-type title + :option-key: option-value + :option-key: option-value + + content text here + ::: + """ + def __init__(self, plugins: list[DirectivePlugin]) -> None: + super().__init__(plugins, markers=":") + _marker_pattern = re.escape(self.markers) + self.directive_pattern = ( + r"^(?P<fenced_directive_mark>(?:" + _marker_pattern + r"){3,})" + r"\s*[a-zA-Z0-9_-]+" + ) + + def _process_directive(self, block: "BlockParser", marker: str, start: int, state: "BlockState") -> int | None: + mlen = len(marker) + cursor_start = start + len(marker) + + _end_pattern = ( + r"^ {0,3}" + marker[0] + "{" + str(mlen) + r",}" + r"[ \t]*(?:\n|$)" + ) + _end_re = re.compile(_end_pattern, re.M) + + _end_m = _end_re.search(state.src, cursor_start) + if _end_m: + text = state.src[cursor_start : _end_m.start()] + end_pos = _end_m.end() + else: + text = state.src[cursor_start:] + end_pos = state.cursor_max + + m = _directive_re.match(text) + if not m: + return None + + self.parse_method(block, m, state) + return end_pos diff --git a/src/writeadoc/md/div.py b/src/writeadoc/md/div.py new file mode 100644 index 0000000..590880f --- /dev/null +++ b/src/writeadoc/md/div.py @@ -0,0 +1,40 @@ +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 Container(DirectivePlugin): + def parse( + self, block: "BlockParser", m: re.Match[str], state: "BlockState" + ) -> dict[str, t.Any]: + attrs = dict(self.parse_options(m)) + attrs.setdefault("class", "") + title = self.parse_title(m) + attrs["class"] += f"{title} {attrs['class']}".strip() + content = self.parse_content(m) + return { + "type": "div", + "children": self.parse_tokens(block, content, state), + "attrs": attrs, + } + + def __call__(self, directive: "BaseDirective", md: "Markdown") -> None: + directive.register("div", self.parse) + + assert md.renderer is not None + if md.renderer.NAME == "html": + md.renderer.register("div", render_container) + + +def render_container(self: t.Any, text: str, **attrs: t.Any) -> str: + html_attrs = render_attrs(attrs) + return f"<div{html_attrs}>\n{text}</div>\n" diff --git a/src/writeadoc/md/figure.py b/src/writeadoc/md/figure.py new file mode 100644 index 0000000..196e59d --- /dev/null +++ b/src/writeadoc/md/figure.py @@ -0,0 +1,41 @@ +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 Figure(DirectivePlugin): + def parse( + self, block: "BlockParser", m: re.Match[str], state: "BlockState" + ) -> dict[str, t.Any]: + attrs = dict(self.parse_options(m)) + attrs["caption"] = (self.parse_title(m) or "").strip() + content = self.parse_content(m) + return { + "type": "figure", + "text": content, + "attrs": attrs, + } + + def __call__(self, directive: "BaseDirective", md: "Markdown") -> None: + directive.register("figure", self.parse) + assert md.renderer is not None + if md.renderer.NAME == "html": + md.renderer.register("figure", render_figure) + + +def render_figure(self: t.Any, text: str, **attrs: t.Any) -> str: + caption = attrs.pop("caption", "") + html_attrs = render_attrs(attrs) + html = f"<figure{html_attrs}>\n{text}\n" + if caption: + html = f"{html}<figcaption>{caption}</figcaption>\n" + return f"{html}</figure>\n" diff --git a/src/writeadoc/md/formatting.py b/src/writeadoc/md/formatting.py new file mode 100644 index 0000000..8173983 --- /dev/null +++ b/src/writeadoc/md/formatting.py @@ -0,0 +1,199 @@ +""" +Mistune plugins to support additional inline formatting. + +Copyright (c) 2014, Hsiaoming Yang +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of the creator nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" +import re +import typing as t + +from mistune.helpers import PREVENT_BACKSLASH + +from .utils import render_attrs + + +if t.TYPE_CHECKING: + from mistune.core import BaseRenderer, InlineState + from mistune.inline_parser import InlineParser + from mistune.markdown import Markdown + + +__all__ = ["strikethrough", "mark", "insert", "superscript", "subscript"] + +_STRIKE_END = re.compile(r"(?:" + PREVENT_BACKSLASH + r"\\~|[^\s~])~~(?!~)") +_MARK_END = re.compile(r"(?:" + PREVENT_BACKSLASH + r"\\=|[^\s=])==(?!=)") +_INSERT_END = re.compile(r"(?:" + PREVENT_BACKSLASH + r"\\\^|[^\s^])\^\^(?!\^)") + +SUPERSCRIPT_PATTERN = r"\^(?:" + PREVENT_BACKSLASH + r"\\\^|\S|\\ )+?\^" +SUBSCRIPT_PATTERN = r"~(?:" + PREVENT_BACKSLASH + r"\\~|\S|\\ )+?~" + + +def parse_strikethrough(inline: "InlineParser", m: re.Match[str], state: "InlineState") -> int | None: + return _parse_to_end(inline, m, state, "strikethrough", _STRIKE_END) + + +def render_strikethrough(renderer: "BaseRenderer", text: str, **attrs: t.Any) -> str: + return f"<del{render_attrs(attrs)}>{text}</del>" + + +def parse_mark(inline: "InlineParser", m: re.Match[str], state: "InlineState") -> int | None: + return _parse_to_end(inline, m, state, "mark", _MARK_END) + + +def render_mark(renderer: "BaseRenderer", text: str, **attrs: t.Any) -> str: + return f"<mark{render_attrs(attrs)}>{text}</mark>" + + +def parse_insert(inline: "InlineParser", m: re.Match[str], state: "InlineState") -> int | None: + return _parse_to_end(inline, m, state, "insert", _INSERT_END) + + +def render_insert(renderer: "BaseRenderer", text: str, **attrs: t.Any) -> str: + return f"<ins{render_attrs(attrs)}>{text}</ins>" + + +def parse_superscript(inline: "InlineParser", m: re.Match[str], state: "InlineState") -> int: + return _parse_script(inline, m, state, "superscript") + + +def render_superscript(renderer: "BaseRenderer", text: str, **attrs: t.Any) -> str: + return f"<sup{render_attrs(attrs)}>{text}</sup>" + + +def parse_subscript(inline: "InlineParser", m: re.Match[str], state: "InlineState") -> int: + return _parse_script(inline, m, state, "subscript") + + +def render_subscript(renderer: "BaseRenderer", text: str, **attrs: t.Any) -> str: + return f"<sub{render_attrs(attrs)}>{text}</sub>" + + +def _parse_to_end( + inline: "InlineParser", + m: re.Match[str], + state: "InlineState", + tok_type: str, + end_pattern: re.Pattern[str], +) -> int | None: + pos = m.end() + m1 = end_pattern.search(state.src, pos) + if not m1: + return None + end_pos = m1.end() + text = state.src[pos : end_pos - 2] + new_state = state.copy() + new_state.src = text + children = inline.render(new_state) + state.append_token({"type": tok_type, "children": children}) + return end_pos + + +def _parse_script(inline: "InlineParser", m: re.Match[str], state: "InlineState", tok_type: str) -> int: + text = m.group(0) + new_state = state.copy() + new_state.src = text[1:-1].replace("\\ ", " ") + children = inline.render(new_state) + state.append_token({"type": tok_type, "children": children}) + return m.end() + + +def strikethrough(md: "Markdown") -> None: + """A mistune plugin to support strikethrough. Spec defined by + GitHub flavored Markdown and commonly used by many parsers: + + .. code-block:: text + + ~~This was mistaken text~~ + + It will be converted into HTML: + + .. code-block:: html + + <del>This was mistaken text</del> + + :param md: Markdown instance + """ + md.inline.register( + "strikethrough", + r"~~(?=[^\s~])", + parse_strikethrough, + before="link", + ) + if md.renderer and md.renderer.NAME == "html": + md.renderer.register("strikethrough", render_strikethrough) + + +def mark(md: "Markdown") -> None: + """A mistune plugin to add ``<mark>`` tag. Spec defined at + https://facelessuser.github.io/pymdown-extensions/extensions/mark/: + + .. code-block:: text + + ==mark me== ==mark \\=\\= equal== + + :param md: Markdown instance + """ + md.inline.register( + "mark", + r"==(?=[^\s=])", + parse_mark, + before="link", + ) + if md.renderer and md.renderer.NAME == "html": + md.renderer.register("mark", render_mark) + + +def insert(md: "Markdown") -> None: + """A mistune plugin to add ``<ins>`` tag. Spec defined at + https://facelessuser.github.io/pymdown-extensions/extensions/caret/#insert: + + .. code-block:: text + + ^^insert me^^ + + :param md: Markdown instance + """ + md.inline.register( + "insert", + r"\^\^(?=[^\s\^])", + parse_insert, + before="link", + ) + if md.renderer and md.renderer.NAME == "html": + md.renderer.register("insert", render_insert) + + +def superscript(md: "Markdown") -> None: + """A mistune plugin to add ``<sup>`` tag. Spec defined at + https://pandoc.org/MANUAL.html#superscripts-and-subscripts: + + .. code-block:: text + + 2^10^ is 1024. + + :param md: Markdown instance + """ + md.inline.register("superscript", SUPERSCRIPT_PATTERN, parse_superscript, before="linebreak") + if md.renderer and md.renderer.NAME == "html": + md.renderer.register("superscript", render_superscript) + + +def subscript(md: "Markdown") -> None: + """A mistune plugin to add ``<sub>`` tag. Spec defined at + https://pandoc.org/MANUAL.html#superscripts-and-subscripts: + + .. code-block:: text + + H~2~O is a liquid. + + :param md: Markdown instance + """ + md.inline.register("subscript", SUBSCRIPT_PATTERN, parse_subscript, before="linebreak") + if md.renderer and md.renderer.NAME == "html": + md.renderer.register("subscript", render_subscript) diff --git a/src/writeadoc/md/highlight.py b/src/writeadoc/md/highlight.py new file mode 100644 index 0000000..0c115b0 --- /dev/null +++ b/src/writeadoc/md/highlight.py @@ -0,0 +1,125 @@ +import re +import typing as t +from collections.abc import Callable + +import mistune +from pygments import highlight +from pygments.formatters.html import HtmlFormatter +from pygments.lexers import get_lexer_by_name + + +class CustomHtmlFormatter(HtmlFormatter): + """Adds ability to output line numbers in a new way.""" + + # Capture `<span class="lineno"> 1 </span>` + RE_SPAN_NUMS = re.compile( + r'(<span[^>]*?)(class="[^"]*\blinenos?\b[^"]*)"([^>]*)>([^<]+)(</span>)' + ) + # Capture `<pre>` that is not followed by `<span></span>` + RE_TABLE_NUMS = re.compile(r"(<pre[^>]*>)(?!<span></span>)") + + def __init__(self, **options): + HtmlFormatter.__init__(self, **options) + + def _format_custom_line(self, m): + """Format the custom line number.""" + return ( + m.group(1) + + 'data-linenos="' + + m.group(4) + + '">' + + m.group(5) + ) + + def _wrap_customlinenums(self, inner): + """ + Wrapper to handle block inline line numbers. + + Don't display line numbers via `<span> 1</span>`, + but include as `<span data-linenos=" 1"></span>` and use CSS to display them: + `[data-linenos]:before {content: attr(data-linenos);}`. + This allows us to use inline and copy and paste without issue. + """ + + for tk, line in inner: + if tk: + line = self.RE_SPAN_NUMS.sub(self._format_custom_line, line) + yield tk, line + + def wrap(self, source): + """Wrap the source code.""" + if self.linenos == 2: # "inline" + source = self._wrap_customlinenums(source) + return HtmlFormatter.wrap(self, source) + + +def block_code( + code: str, info: str | None, *, escape: Callable[[str], str] = mistune.escape +) -> str: + code = code.strip() + info = (info or "").strip() + + if not info: + return f"<pre><code>{escape(code)}</code></pre>\n" + + lang, *attrs = info.split(maxsplit=1) + options = parse_attrs(attrs[0].strip() if attrs else "") + options["cssclass"] = f"highlight lang-{lang}" + options["wrapcode"] = True + + try: + lexer = get_lexer_by_name(lang, stripall=True) + formatter = CustomHtmlFormatter(**options) + result = highlight(code, lexer, formatter) + return result.replace("<pre><span></span><code>", "<pre><code>") + + except Exception: + return f'<div class="lang-{lang}"><pre><code>{escape(code)}</code></pre></div>\n' + + +def parse_attrs(attrs_str: str) -> dict[str, t.Any]: + attrs = { + "linenos": False, + "hl_lines": [], + "linenostart": 1, + "linenostep": 1, + "filename": None, + } + + # Surronding braces are optional + attrs_str = attrs_str.lstrip("{").rstrip("}").strip() + if not attrs_str: + return attrs + + attrs_dict = dict(re.findall(r'(?P<key>\w+)="(?P<value>[^"]*)"', attrs_str)) + attrs = attrs_dict.copy() + + if "title" in attrs_dict: + attrs["filename"] = attrs_dict["title"] + + if "linenums" in attrs_dict: + attrs["linenos"] = "inline" + start, *step = attrs_dict["linenums"].split() + if start.isdigit(): + attrs["linenostart"] = int(start) + if step and step[0].isdigit(): + attrs["linenostep"] = int(step[0]) + + if "hl_lines" in attrs_dict: + attrs["hl_lines"] = [] + for val in attrs_dict["hl_lines"].split(): + if "-" in val: + start, end = val.split("-", 1) + if start.isdigit() and end.isdigit(): + attrs["hl_lines"].extend( + range(int(start), int(end) + 1) + ) + elif val.isdigit(): + attrs["hl_lines"].append(int(val)) + + return attrs + + +class HighlightMixin(object): + def block_code(self, code, info=None): + return block_code(code, info or "") diff --git a/src/writeadoc/md/html_renderer.py b/src/writeadoc/md/html_renderer.py new file mode 100644 index 0000000..46678f7 --- /dev/null +++ b/src/writeadoc/md/html_renderer.py @@ -0,0 +1,82 @@ +import typing as t + +import mistune +from mistune.util import escape, striptags + +from .highlight import HighlightMixin +from .utils import render_attrs + + +class HTMLRenderer(HighlightMixin, mistune.HTMLRenderer): + + def emphasis(self, text: str, **attrs: t.Any) -> str: + return f"<em{render_attrs(attrs)}>{text}</em>" + + def strong(self, text: str, **attrs: t.Any) -> str: + return f"<strong{render_attrs(attrs)}>{text}</strong>" + + def link(self, text: str, url: str, title: str | None = None, **attrs: t.Any) -> str: + attrs["href"] = url + if title: + attrs["title"] = title + return f"<a{render_attrs(attrs)}>{text}</a>" + + def image(self, text: str, url: str, title: str | None = None, **attrs: t.Any) -> str: + attrs["src"] = url + attrs["alt"] = escape(striptags(text)) + if title: + attrs["title"] = title + return f"<img{render_attrs(attrs)} />" + + def codespan(self, text: str, **attrs: t.Any) -> str: + return f"<code{render_attrs(attrs)}>{escape(text)}</code>" + + def paragraph(self, text: str, **attrs: t.Any) -> str: + return f"<p{render_attrs(attrs)}>{text}</p>\n" + + def heading(self, text: str, level: int, **attrs: t.Any) -> str: + return f"<h{level}{render_attrs(attrs)}>{text}</h{level}>\n" + + def thematic_break(self, **attrs: t.Any) -> str: + return f"<hr{render_attrs(attrs)}/>\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"<blockquote{render_attrs(attrs)}>{text}</blockquote>\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"<ol{render_attrs(attrs)}>\n{text}</ol>\n" + return f"<ul{render_attrs(attrs)}>\n{text}</ul>\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"<li{render_attrs(attrs)}>{text}</li>\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 "<br />\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'<input id="{tab_id}" name="__tabbed_{set_id}" type="radio"{checked}>' + ) + labels.append(f'<label for="{tab_id}">{tab["label_html"] or i + 1}</label>') + panels.append(f'<div class="tabbed-panel">\n{tab["content_html"]}</div>') + + return ( + '<div class="tabbed-set">\n' + + "\n".join(inputs) + + "\n" + + '<div class="tabbed-labels">\n' + + "\n".join(labels) + + "\n</div>\n" + + '<div class="tabbed-panels">\n' + + "\n".join(panels) + + "\n</div>\n" + + "</div>\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"<p>\s*:::\s+([\w\.\:]+)((?:\s+\w+=[\w\*_]+)*)\s*</p>") - - class PagesProcessor: docs: "Docs" - md_renderer: markdown.Markdown - autodoc: Autodoc - nav_items: list[NavItem] pages: list[PageData] def __init__(self, docs: "Docs"): - """Pages processor - """ + """Pages processor""" self.docs = docs - self.md_renderer = markdown.Markdown( - extensions=[*utils.DEFAULT_MD_EXTENSIONS], - extension_configs={**utils.DEFAULT_MD_CONFIG}, - output_format="html", - tab_length=2, - ) - self.autodoc = Autodoc() self.pages = [] - def run(self, user_pages: TUserPages) -> tuple[list[NavItem], list[PageData]]: + def run(self, user_pages: Sequence[str | dict[str, t.Any]]) -> tuple[list[NavItem], list[PageData]]: """Recursively process the given pages list and returns navigation and flat page list. Input: @@ -172,7 +155,7 @@ def process_index_page(self) -> PageData | None: md_index = self.docs.content_dir / self.docs.prefix / "index.md" if md_index.exists(): source, meta = self.read_file(md_index) - html = self.render_markdown(source, meta) + html, state = self.render_markdown(source, meta) meta.setdefault("id", "index") meta.setdefault("title", self.docs.site.name) meta.setdefault("view", "index.jinja") @@ -183,7 +166,7 @@ def process_index_page(self) -> PageData | None: source=source, content=Markup(html), filepath=md_index, - toc=getattr(self.md_renderer, "toc_tokens", []), + toc=state.get("toc_items", []), ) # Just render the template page @@ -198,7 +181,7 @@ def process_index_page(self) -> PageData | None: def process_items( self, - user_pages: TUserPages, + user_pages: Sequence[str | dict[str, t.Any]], section_title: str = "", section_url: str = "", parents: tuple[str, ...] = (), @@ -233,7 +216,7 @@ def process_items( def process_section( self, - user_page: TUserSection, + user_page: dict[str, t.Any], section_title: str = "", section_url: str = "", parents: tuple[str, ...] = (), @@ -249,7 +232,7 @@ def process_section( url = "" id = user_page.get("id") or f"s-{uuid4().hex}" - parents = parents + (id, ) + parents = parents + (id,) sec_path = user_page.get("path") if sec_path: @@ -272,14 +255,7 @@ def process_section( section_url=url, parents=parents, ) - return NavItem( - title=title, - id=id, - url=url, - icon=icon, - pages=pages, - closed=closed - ) + return NavItem(title=title, id=id, url=url, icon=icon, pages=pages, closed=closed) def process_page( self, @@ -294,8 +270,13 @@ def process_page( filepath = self.docs.content_dir / filename source, meta = self.read_file(filepath) + + def _render(**globals: t.Any) -> str: + return self.docs.catalog.render("autodoc.md.jinja", **globals) + + source = render_autodoc(source.strip(), render=_render) try: - html = self.render_markdown(source, meta) + html, state = self.render_markdown(source, meta) except Exception as err: raise RuntimeError(f"Error processing {filepath}") from err @@ -307,7 +288,7 @@ def process_page( source=source, content=Markup(html), filepath=filepath, - toc=getattr(self.md_renderer, "toc_tokens", []), + toc=state.get("toc_items", []), parents=parents, ) self.pages.append(page) @@ -328,58 +309,24 @@ def read_file(self, filepath: Path) -> tuple[str, TMetadata]: source, meta = utils.extract_metadata(source) return source, meta - def render_markdown(self, source: str, meta: TMetadata) -> str: + def render_markdown(self, source: str, meta: TMetadata) -> tuple[str, MutableMapping]: source = source.strip() - self.md_renderer.reset() - html = self.md_renderer.convert(source).strip() - html = html.replace("<pre><span></span>", "<pre>") - html = self.render_autodoc(html) + html, state = render_markdown(source) if imports := meta.get("imports"): if not isinstance(imports, dict): raise ValueError("Invalid 'imports' in metadata, must be a dict") - html = self._render_mdjx(html, imports) - - return html - - def render_autodoc(self, html: str): - while True: - match = RX_AUTODOC.search(html) - if not match: - break - name = match.group(1) - - kwargs: dict[str, t.Any] = dict(arg.split("=") for arg in match.group(2).split()) - - show_name = kwargs.pop("name", "1").lower() not in ("false", "0", "no") - show_members = kwargs.pop("members", "1").lower() not in ("false", "0", "no") - include = (kwargs.pop("include", "").split(",")) if "include" in kwargs else () - exclude = (kwargs.pop("exclude", "").split(",")) if "exclude" in kwargs else () - kwargs["ds"] = self.autodoc( - name, - show_name=show_name, - show_members=show_members, - include=include, - exclude=exclude, - ) - if "level" in kwargs: - kwargs["level"] = int(kwargs["level"]) - - try: - frag = self.docs.catalog.render("autodoc.jinja", **kwargs) - except jx.JxException as err: - raise RuntimeError(f"Error rendering autodoc for {name}") from err - frag = str(frag).replace("<br>", "").strip() - start, end = match.span(0) - html = f"{html[:start]}{frag}{html[end:]}" + html = self.render_mdjx(html, imports) - return html + return html, state - def _render_mdjx(self, source: str, imports: dict[str, str]) -> str: + def render_mdjx(self, source: str, imports: dict[str, str]) -> str: OPEN_REPL = "\u0002" CLOSE_REPL = "\u0003" source = source.replace("{", OPEN_REPL).replace("}", CLOSE_REPL) - jx_imports = "\n".join(f'{{# import "{path}" as {name} #}}' for name, path in imports.items()) + jx_imports = "\n".join( + f'{{# import "{path}" as {name} #}}' for name, path in imports.items() + ) html = self.docs.catalog.render_string(f"{jx_imports}\n{source}") html = str(html).replace(OPEN_REPL, "{").replace(CLOSE_REPL, "}") return html @@ -397,7 +344,7 @@ def set_prev_next(self) -> None: id=prev_page.id, title=prev_page.title, url=prev_page.url, - section=prev_page.section_title + section=prev_page.section_title, ) else: page.prev = None @@ -408,7 +355,7 @@ def set_prev_next(self) -> None: id=next_page.id, title=next_page.title, url=next_page.url, - section=next_page.section_title + section=next_page.section_title, ) else: page.next = None diff --git a/src/writeadoc/types.py b/src/writeadoc/types.py index ee22d38..e9f6143 100644 --- a/src/writeadoc/types.py +++ b/src/writeadoc/types.py @@ -1,5 +1,4 @@ import typing as t -from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path from uuid import uuid4 @@ -7,8 +6,6 @@ __all__ = ( "TMetadata", - "TUserSection", - "TUserPages", "PageRef", "TSearchPageData", "NavItem", @@ -20,16 +17,6 @@ TMetadata = dict[str, t.Any] -class TUserSection(t.TypedDict): - title: str - path: str - icon: str | None - pages: "TUserPages" - - -TUserPages = Sequence[str | TUserSection] - - class TSearchPageData(t.TypedDict): """ SearchData represents the data structure for search functionality. diff --git a/src/writeadoc/utils.py b/src/writeadoc/utils.py index 8e0b04b..52fdcc2 100644 --- a/src/writeadoc/utils.py +++ b/src/writeadoc/utils.py @@ -24,64 +24,6 @@ logger.setLevel(logging.WARNING) logger.addHandler(logging.StreamHandler()) -DEFAULT_MD_EXTENSIONS = [ - "attr_list", - "md_in_html", - "tables", - "pymdownx.betterem", - "pymdownx.blocks.admonition", - "pymdownx.blocks.details", - "pymdownx.caret", - "pymdownx.fancylists", - "pymdownx.highlight", - "pymdownx.inlinehilite", - "pymdownx.mark", - "pymdownx.saneheaders", - "pymdownx.smartsymbols", - "pymdownx.superfences", - "pymdownx.tasklist", - "pymdownx.tilde", - # "writeadoc.ext.jx", - "writeadoc.ext.pagetoc", - "writeadoc.ext.tab", -] - -DEFAULT_MD_CONFIG = { - "keys": { - "camel_case": True, - }, - "writeadoc.pagetoc": { - "permalink": True, - "permalink_title": "", - "toc_depth": 3, - }, - "pymdownx.blocks.admonition": { - "types": [ - "note", - "tip", - "warning", - "danger", - "new", - "question", - "error", - "example", - ], - }, - "pymdownx.fancylists": { - "additional_ordered_styles": ["roman", "alpha", "generic"], - "inject_class": True, - }, - "pymdownx.highlight": { - "linenums_style": "pymdownx-inline", - "anchor_linenums": False, - "css_class": "highlight", - "pygments_lang_class": True, - }, - "pymdownx.superfences": { - "disable_indented_code_blocks": True, - }, -} - META_START = "---" META_END = "\n---" diff --git a/src/writeadoc/ext/__init__.py b/tests/md/__init__.py similarity index 100% rename from src/writeadoc/ext/__init__.py rename to tests/md/__init__.py diff --git a/tests/md/test.html b/tests/md/test.html new file mode 100644 index 0000000..ae21f9b --- /dev/null +++ b/tests/md/test.html @@ -0,0 +1 @@ +<p>Lorem Ipsum</p> \ No newline at end of file diff --git a/tests/md/test.md b/tests/md/test.md new file mode 100644 index 0000000..716ed14 --- /dev/null +++ b/tests/md/test.md @@ -0,0 +1 @@ +# Hello world diff --git a/tests/md/test_attrs.py b/tests/md/test_attrs.py new file mode 100644 index 0000000..b11af0d --- /dev/null +++ b/tests/md/test_attrs.py @@ -0,0 +1,181 @@ +import pytest + +from writeadoc.md import render_markdown + + +TEST_CASES = [ + ( # Classes shortcut + """![Nav A](/assets/images/nav-page-light.png){ .only-light .right }""", + """<p><img alt="Nav A" class="only-light right" src="/assets/images/nav-page-light.png" /></p> +""" + ), + + ( # Classes shortcut + attribute + """![Nav A](/assets/images/nav-page-light.png){ .right class="only-light" }""", + # first the attr, then the shortcut(s) + """<p><img alt="Nav A" class="only-light right" src="/assets/images/nav-page-light.png" /></p> +""" + ), + + ( # ID shortcut + """[Meh](#meh){ #green }""", + """<p><a href="#meh" id="green">Meh</a></p> +""" + ), + + ( # ID shortcut + attr + """[Meh](#meh){ #green id="red" }""", + # last one defined wins + """<p><a href="#meh" id="red">Meh</a></p> +""" + ), + + ( # emphasis + """a *b*{ .bla } c""", + """<p>a <em class="bla">b</em> c</p> +""" + ), + + ( # strong + """a **b**{ .bla } c""", + """<p>a <strong class="bla">b</strong> c</p> +""" + ), + + ( # codespan + """a `b`{ .bla } c""", + """<p>a <code class="bla">b</code> c</p> +""" + ), + + ( # paragraph + """ +lorem ipsum +{ .fancy } +""", + """<p class="fancy">lorem ipsum</p> +""" + ), + + ( # heading + """ +# Heading 1 +{ .fancy } + +# Heading 2 +{ .fancy } + +# Heading 3 +{ .fancy } +""", + """<h1 class="fancy" id="heading-1">Heading 1</h1> +<h1 class="fancy" id="heading-2">Heading 2</h1> +<h1 class="fancy" id="heading-3">Heading 3</h1> +""" + ), + + ( # thematic_break + """ +---- +{ .fancy } +""", + """<hr class="fancy"/> +""" + ), + + ( # block_quote (ignore attrs) + """ +> This is the first line of the quote. +> This is the second line of the quote. +{ .fancy } +""", + """<blockquote><p>This is the first line of the quote. +This is the second line of the quote. +</p> +</blockquote> +""" + ), + + ( # ul list (ignore attrs) + """ +* One +* Two +* Three +{ .fancy } +""", + """<ul depth="0"> +<li>One</li> +<li>Two</li> +<li>Three +</li> +</ul> +""" + ), + + ( # ol list (ignore attrs) + """ +1. One +2. Two +3. Three +{ .fancy } +""", + """<ol depth="0"> +<li>One</li> +<li>Two</li> +<li>Three +</li> +</ol> +""" + ), + + ( # list_item (ignore attrs) + """ +* One +* Two{ .fancy } +* Three +""", + """<ul depth="0"> +<li>One</li> +<li>Two</li> +<li>Three</li> +</ul> +""" + ), + + ( # strikethrough + """~~here is the content~~{ .bla }""", + """<p><del class="bla">here is the content</del></p> +""" + ), + + ( # insert + r"""^^insert me^^ ^^insert\^\^me^^{ .bla }""", + """<p><ins>insert me</ins> <ins class="bla">insert^^me</ins></p> +""" + ), + + ( # mark + r"""==mark me== ==mark with\=\=equal=={ .bla }""", + """<p><mark>mark me</mark> <mark class="bla">mark with==equal</mark></p> +""" + ), + + ( # subscript + """Hello~subscript~{ .bla }""", + """<p>Hello<sub class="bla">subscript</sub></p> +""" + ), + + ( # superscript + """Hello^superscript^{ .bla }""", + """<p>Hello<sup class="bla">superscript</sup></p> +""" + ), +] + + +@pytest.mark.parametrize("source, expected", TEST_CASES) +def test_render_attrs(source, expected): + result = render_markdown(source)[0] + print(result) + assert result == expected diff --git a/tests/md/test_highlight.py b/tests/md/test_highlight.py new file mode 100644 index 0000000..219f40f --- /dev/null +++ b/tests/md/test_highlight.py @@ -0,0 +1,166 @@ +import pytest + +from writeadoc.md import render_markdown + + +TEST_CASES = [ + ( # raw code block + """ +``` +console.log("Hello world"); +``` +""", + """<pre><code>console.log("Hello world");</code></pre> +""", + ), + + ( # language specified + """ +```javascript +console.log("Hello world"); +``` +""", + """<div class="highlight lang-javascript"><pre><code><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Hello world"</span><span class="p">);</span> +</code></pre></div> +""", + ), + + ( # optional braces + """ +```python {linenums="1"} +import foo.bar + +a = "lorem" +b = "ipsum" +``` +""", + """<div class="highlight lang-python"><pre><code><span data-linenos="1"></span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar</span> +<span data-linenos="2"></span> +<span data-linenos="3"></span><span class="n">a</span> <span class="o">=</span> <span class="s2">"lorem"</span> +<span data-linenos="4"></span><span class="n">b</span> <span class="o">=</span> <span class="s2">"ipsum"</span> +</code></pre></div> +""" + ), + + ( # extra spaces + """ +```python linenums="1" +import foo.bar + +a = "lorem" +b = "ipsum" +``` +""", + """<div class="highlight lang-python"><pre><code><span data-linenos="1"></span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar</span> +<span data-linenos="2"></span> +<span data-linenos="3"></span><span class="n">a</span> <span class="o">=</span> <span class="s2">"lorem"</span> +<span data-linenos="4"></span><span class="n">b</span> <span class="o">=</span> <span class="s2">"ipsum"</span> +</code></pre></div> +""" + ), + + ( # linenums starting != 1 + """ +```python {linenums="42"} +import foo.bar + +a = "lorem" +b = "ipsum" +``` +""", + """<div class="highlight lang-python"><pre><code><span data-linenos="42"></span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar</span> +<span data-linenos="43"></span> +<span data-linenos="44"></span><span class="n">a</span> <span class="o">=</span> <span class="s2">"lorem"</span> +<span data-linenos="45"></span><span class="n">b</span> <span class="o">=</span> <span class="s2">"ipsum"</span> +</code></pre></div> +""" + ), + + ( # multiple linenums + """ +```python {linenums="1 2"} +import foo.bar + +a = "lorem" +b = "ipsum" +``` +""", + """<div class="highlight lang-python"><pre><code><span data-linenos=" "></span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar</span> +<span data-linenos="2"></span> +<span data-linenos=" "></span><span class="n">a</span> <span class="o">=</span> <span class="s2">"lorem"</span> +<span data-linenos="4"></span><span class="n">b</span> <span class="o">=</span> <span class="s2">"ipsum"</span> +</code></pre></div> +""" + ), + + ( # multiple hl_lines + ''' +```python {hl_lines="1 3"} +"""Some file.""" + +import boo.baz +import foo.bar.baz +``` +''', + """<div class="highlight lang-python"><pre><code><span class="hll"><span class="sd">"""Some file."""</span> +</span> +<span class="hll"><span class="kn">import</span><span class="w"> </span><span class="nn">boo.baz</span> +</span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar.baz</span> +</code></pre></div> +""" + ), + + ( # range of hl_lines + ''' +```python {hl_lines="1-3"} +"""Some file.""" + +import boo.baz +import foo.bar.baz +``` +''', + """<div class="highlight lang-python"><pre><code><span class="hll"><span class="sd">"""Some file."""</span> +</span><span class="hll"> +</span><span class="hll"><span class="kn">import</span><span class="w"> </span><span class="nn">boo.baz</span> +</span><span class="kn">import</span><span class="w"> </span><span class="nn">foo.bar.baz</span> +</code></pre></div> +""" + ), + + ( # hl_lines with linenums + """ +```python {linenums="42" hl_lines="2"} +def foobar(): + a = "lorem" + b = "ipsum" + +foobar() +``` +""", + """<div class="highlight lang-python"><pre><code><span data-linenos="42"></span><span class="k">def</span><span class="w"> </span><span class="nf">foobar</span><span class="p">():</span> +<span class="hll"><span data-linenos="43"></span> <span class="n">a</span> <span class="o">=</span> <span class="s2">"lorem"</span> +</span><span data-linenos="44"></span> <span class="n">b</span> <span class="o">=</span> <span class="s2">"ipsum"</span> +<span data-linenos="45"></span> +<span data-linenos="46"></span><span class="n">foobar</span><span class="p">()</span> +</code></pre></div> +""" + ), + + ( # filename + """ +```python {title="cool_file.py"} +import foo +``` +""", + """<div class="highlight lang-python"><span class="filename">cool_file.py</span><pre><code><span class="kn">import</span><span class="w"> </span><span class="nn">foo</span> +</code></pre></div> +""" + ), +] + + +@pytest.mark.parametrize("source, expected", TEST_CASES) +def test_render_highlight(source, expected): + result = render_markdown(source)[0] + print(result) + assert result == expected diff --git a/tests/md/test_plugins.py b/tests/md/test_plugins.py new file mode 100644 index 0000000..03958bc --- /dev/null +++ b/tests/md/test_plugins.py @@ -0,0 +1,194 @@ +import pytest + +from writeadoc.md import render_markdown + + +TEST_CASES = [ + ( # abbr + """ +The HTML specification +is maintained by the W3C. + +*[HTML]: Hyper Text Markup Language +*[W3C]: World Wide Web Consortium +""", + """<p>The <abbr title="Hyper Text Markup Language">HTML</abbr> specification +is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>.</p> +"""), + + ( # footnotes + """ +content in paragraph with footnote[^1] markup. + +[^1]: footnote explain +""", + """<p>content in paragraph with footnote<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup> markup.</p> +<section class="footnotes"> +<ol> +<li id="fn-1"><p>footnote explain<a href="#fnref-1" class="footnote">↩</a></p></li> +</ol> +</section> +"""), + + ( # tables + """ +| Left Header | Center Header | Right Header | +| :----------- | :-------------: | ------------: | +| Content Cell | Content Cell | Content Cell | +""", + """<table> +<thead> +<tr> + <th style="text-align:left">Left Header</th> + <th style="text-align:center">Center Header</th> + <th style="text-align:right">Right Header</th> +</tr> +</thead> +<tbody> +<tr> + <td style="text-align:left">Content Cell</td> + <td style="text-align:center">Content Cell</td> + <td style="text-align:right">Content Cell</td> +</tr> +</tbody> +</table> +"""), + + ( # task lists + """ +- [x] item 1 +- [ ] item 2 +""", + """<ul depth="0"> +<li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>item 1</li> +<li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>item 2</li> +</ul> +"""), + + ( # def_list + """ +First term +: First definition +: Second definition + +Second term +: Third definition +""", + """<dl> +<dt>First term</dt> +<dd>First definition</dd> +<dd>Second definition</dd> +<dt>Second term</dt> +<dd>Third definition</dd> +</dl> +"""), + + ( # admonition + """ +::: note +This is a note admonition +::: +""", + """<section class="admonition note"> +<p class="admonition-title">Note</p> +<p>This is a note admonition</p> +</section> +"""), + + ( # admonition with title + """ +::: note Custom Title +This is a note admonition +::: +""", + """<section class="admonition note"> +<p class="admonition-title">Custom Title</p> +<p>This is a note admonition</p> +</section> +"""), + + ( # details + """ +::: note +:open: true + +This is a note admonition +::: +""", + """<details class="admonition note" open> +<summary class="admonition-title">Note</summary> +<p>This is a note admonition</p> +</details> +"""), + + ( # details with title + """ +::: note Custom Title +:open: true + +This is a note admonition +::: +""", + """<details class="admonition note" open> +<summary class="admonition-title">Custom Title</summary> +<p>This is a note admonition</p> +</details> +"""), + + ( # details closed + """ +::: note +:open: false + +This is a note admonition +::: +""", + """<details class="admonition note"> +<summary class="admonition-title">Note</summary> +<p>This is a note admonition</p> +</details> +"""), + + ( # include markdown + """ +::: include test.md +::: +""", + """<h1 id="hello-world">Hello world</h1> +"""), + + ( # include html + """ +::: include test.html +::: +""", + """<p>Lorem Ipsum</p> +"""), + + ( # include error + """ +::: include nonexistent.md +::: +""", + """<div class="error"><pre>Could not find file: nonexistent.md</pre></div> +"""), + + ( # container + """ +::: div grid + +This is *inside* a container. +::: +""", + """<div class="grid"> +<p>This is <em>inside</em> a container.</p> +</div> +"""), +] + + +@pytest.mark.parametrize("source, expected", TEST_CASES) +def test_render_plugins(source, expected): + result = render_markdown(source, __file__=__file__)[0] + print(result) + assert result == expected diff --git a/tests/md/test_tab.py b/tests/md/test_tab.py new file mode 100644 index 0000000..5abbd34 --- /dev/null +++ b/tests/md/test_tab.py @@ -0,0 +1,287 @@ +import pytest + +from writeadoc.md import render_markdown + + +TEST_CASES = [ + ( # basic two tabs + """ +::: tab | Label 1 +Content 1 +::: + +::: tab | Label 2 +Content 2 +::: +""", + """<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<input id="__tabbed_1_2" name="__tabbed_1" type="radio"> +<div class="tabbed-labels"> +<label for="__tabbed_1_1">Label 1</label> +<label for="__tabbed_1_2">Label 2</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p>Content 1</p> +</div> +<div class="tabbed-panel"> +<p>Content 2</p> +</div> +</div> +</div> +"""), + + ( # markdown in labels + """ +::: tab | **Bold** Label +Content A +::: + +::: tab | _Italic_ Label +Content B +::: +""", + """<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<input id="__tabbed_1_2" name="__tabbed_1" type="radio"> +<div class="tabbed-labels"> +<label for="__tabbed_1_1"><strong>Bold</strong> Label</label> +<label for="__tabbed_1_2"><em>Italic</em> Label</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p>Content A</p> +</div> +<div class="tabbed-panel"> +<p>Content B</p> +</div> +</div> +</div> +"""), + + ( # markdown in content + """ +::: tab | Tab 1 +**Bold** and _italic_ content + +- List item 1 +- List item 2 +::: + +::: tab | Tab 2 +> A blockquote +::: +""", + """<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<input id="__tabbed_1_2" name="__tabbed_1" type="radio"> +<div class="tabbed-labels"> +<label for="__tabbed_1_1">Tab 1</label> +<label for="__tabbed_1_2">Tab 2</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p><strong>Bold</strong> and <em>italic</em> content</p> +<ul depth="1"> +<li>List item 1</li> +<li>List item 2</li> +</ul> +</div> +<div class="tabbed-panel"> +<blockquote><p>A blockquote</p> +</blockquote> +</div> +</div> +</div> +"""), + + ( # tabs with surrounding content + """ +Before tabs + +::: tab | Tab A +Inside A +::: + +::: tab | Tab B +Inside B +::: + +After tabs +""", + """<p>Before tabs</p> +<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<input id="__tabbed_1_2" name="__tabbed_1" type="radio"> +<div class="tabbed-labels"> +<label for="__tabbed_1_1">Tab A</label> +<label for="__tabbed_1_2">Tab B</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p>Inside A</p> +</div> +<div class="tabbed-panel"> +<p>Inside B</p> +</div> +</div> +</div> +<p>After tabs</p> +"""), + + ( # single tab + """ +::: tab | Solo Tab +Solo content +::: +""", + """<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<div class="tabbed-labels"> +<label for="__tabbed_1_1">Solo Tab</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p>Solo content</p> +</div> +</div> +</div> +"""), + + ( # empty label + """ +::: tab | +No label content +::: + +::: tab | Has Label +With label +::: +""", + """<div class="tabbed-set"> +<input id="__tabbed_1_1" name="__tabbed_1" type="radio" checked> +<input id="__tabbed_1_2" name="__tabbed_1" type="radio"> +<div class="tabbed-labels"> +<label for="__tabbed_1_1">1</label> +<label for="__tabbed_1_2">Has Label</label> +</div> +<div class="tabbed-panels"> +<div class="tabbed-panel"> +<p>No label content</p> +</div> +<div class="tabbed-panel"> +<p>With label</p> +</div> +</div> +</div> +"""), +] + + +@pytest.mark.parametrize("source, expected", TEST_CASES) +def test_render_tabs(source, expected): + result = render_markdown(source)[0] + print(result) + assert result == expected + + +def test_multiple_tab_sets(): + """Test that multiple tab sets get unique IDs.""" + source = """ +::: tab | Set1 Tab1 +Content 1 +::: + +::: tab | Set1 Tab2 +Content 2 +::: + +Some text between sets + +::: tab | Set2 Tab1 +Content A +::: + +::: tab | Set2 Tab2 +Content B +::: +""" + result, env = render_markdown(source) + + # Check that we have two tab sets + assert env.get("_tab_set_counter") == 2 + + # Check IDs are unique + assert "__tabbed_1_1" in result + assert "__tabbed_1_2" in result + assert "__tabbed_2_1" in result + assert "__tabbed_2_2" in result + + # Check names are correct for grouping + assert 'name="__tabbed_1"' in result + assert 'name="__tabbed_2"' in result + + +def test_new_option_forces_new_group(): + """Test that :new: true forces a tab to start a new group.""" + source = """ +::: tab | Tab A +Content A +::: + +::: tab | Tab B +Content B +::: + +::: tab | Tab C +:new: true + +New group content +::: + +::: tab | Tab D +Also in new group +::: +""" + result, env = render_markdown(source) + + # Check that we have two tab sets + assert env.get("_tab_set_counter") == 2 + + # First group has Tab A and Tab B + assert "__tabbed_1_1" in result + assert "__tabbed_1_2" in result + + # Second group has Tab C and Tab D + assert "__tabbed_2_1" in result + assert "__tabbed_2_2" in result + + # Verify the labels are in the right groups + assert 'name="__tabbed_1"' in result + assert 'name="__tabbed_2"' in result + + +def test_select_option(): + """Test that :select: true makes a tab selected by default.""" + source = """ +::: tab | Tab 1 +Content 1 +::: + +::: tab | Tab 2 +:select: true + +Selected by default +::: + +::: tab | Tab 3 +Content 3 +::: +""" + result, _ = render_markdown(source) + + # Tab 2 should be checked, not Tab 1 + assert 'id="__tabbed_1_1" name="__tabbed_1" type="radio">' in result # no checked + assert 'id="__tabbed_1_2" name="__tabbed_1" type="radio" checked>' in result + assert 'id="__tabbed_1_3" name="__tabbed_1" type="radio">' in result # no checked diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 93b04bf..d23cf6f 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1,6 +1,16 @@ from docstring_parser.common import DocstringParam -from writeadoc.autodoc import Autodoc +from writeadoc.autodoc import ( + autodoc, + autodoc_attr, + autodoc_class, + autodoc_function, + autodoc_obj, + autodoc_property, + get_signature, + render_autodoc, + split_description, +) class SimpleClass: @@ -24,6 +34,10 @@ def __init__(self, param1, param2=None): self.param2 = param2 self._private = "private" + def noargs_method(self): + """A test method.""" + pass + def method(self, x, y=0): """A test method. @@ -72,10 +86,6 @@ def sample_function(a, b=1, *, c=2): return a + b + c -# Simple renderer that returns input as-is -autodoc = Autodoc() - - def test_autodoc_parse_function(): """Test autodoc_function function.""" doc = autodoc("tests.test_autodoc.sample_function") @@ -107,7 +117,7 @@ def test_autodoc_parse_class(): assert doc.attrs[1].name == "attr2" # Check methods - assert len(doc.methods) == 1 + assert len(doc.methods) == 2 method = doc.methods[0] assert method.name == "method" assert method.symbol == "function" @@ -127,7 +137,7 @@ def test_autodoc_parse_class(): def test_autodoc_function(): """Test autodoc_function function.""" - doc = autodoc.autodoc_function(sample_function) + doc = autodoc_function(sample_function) assert doc.name == "sample_function" assert doc.symbol == "function" @@ -143,7 +153,7 @@ def test_autodoc_function(): def test_autodoc_class(): """Test autodoc_class function.""" - doc = autodoc.autodoc_class(SimpleClass) + doc = autodoc_class(SimpleClass) assert doc.name == "SimpleClass" assert doc.symbol == "class" @@ -156,7 +166,7 @@ def test_autodoc_class(): assert doc.attrs[1].name == "attr2" # Check methods - assert len(doc.methods) == 1 + assert len(doc.methods) == 2 method = doc.methods[0] assert method.name == "method" assert method.symbol == "function" @@ -176,7 +186,7 @@ def test_autodoc_class(): def test_autodoc_property(): """Test autodoc_property function.""" - doc = autodoc.autodoc_property("prop", SimpleClass.prop) + doc = autodoc_property("prop", SimpleClass.prop) assert doc.name == "prop" assert doc.symbol == "attr" @@ -196,7 +206,7 @@ def test_autodoc_attr(): default=None ) - doc = autodoc.autodoc_attr(param) + doc = autodoc_attr(param) assert doc.name == "test_param: str" assert doc.symbol == "attr" @@ -205,14 +215,13 @@ def test_autodoc_attr(): assert doc.long_description == "Multiple paragraphs." -def test_get_signature(): - """Test get_signature function.""" - # Test simple signature - sig = autodoc.get_signature("simple", lambda x: x) +def test_get_signature_simple(): + sig = get_signature("simple", lambda x: x, max_width=99) assert sig == "simple(x)" - # Test complex signature - sig = autodoc.get_signature("complex", sample_function) + +def test_get_signature_complex(): + sig = get_signature("complex", sample_function, max_width=1) print(sig) assert sig == """ complex( @@ -222,8 +231,21 @@ def test_get_signature(): c=2 )""".strip() - # Test signature with self parameter - sig = autodoc.get_signature("method", SimpleClass.method) + +def test_get_signature_with_self(): + sig = get_signature("method", SimpleClass.method, max_width=99) + print(sig) + assert sig == "method(x, y=0)" + + +def test_get_signature_with_self_noargs(): + sig = get_signature("noargs_method", SimpleClass.noargs_method, max_width=99) + print(sig) + assert sig == "noargs_method()" + + +def test_get_signature_with_self_multiline(): + sig = get_signature("method", SimpleClass.method, max_width=1) print(sig) assert sig == """ method( @@ -232,15 +254,21 @@ def test_get_signature(): )""".strip() +def test_get_signature_max_width(): + sig = get_signature("method", SimpleClass.method, max_width=99) + print(sig) + assert sig == "method(x, y=0)" + + def test_split_description(): """Test split_description function.""" # Single paragraph - short, long = autodoc.split_description("Single paragraph.") + short, long = split_description("Single paragraph.") assert short == "Single paragraph." assert long == "" # Multiple paragraphs - short, long = autodoc.split_description("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.") + short, long = split_description("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.") assert short == "First paragraph." assert long == "Second paragraph.\n\nThird paragraph." @@ -259,24 +287,24 @@ def test_autodoc_full_path(): def test_autodoc_obj(): """Test autodoc_obj function with different types.""" # Class - class_doc = autodoc.autodoc_obj(SimpleClass) + class_doc = autodoc_obj(SimpleClass) assert class_doc.name == "SimpleClass" assert class_doc.symbol == "class" # Function - func_doc = autodoc.autodoc_obj(sample_function) + func_doc = autodoc_obj(sample_function) assert func_doc.name == "sample_function" assert func_doc.symbol == "function" # Other object (should return empty Autodoc) - other_doc = autodoc.autodoc_obj(42) + other_doc = autodoc_obj(42) assert other_doc.name == "" assert other_doc.symbol == "" def test_docstring_special_attributes(): """Test handling of special docstring attributes like example and return values.""" - doc = autodoc.autodoc_function(sample_function) + doc = autodoc_function(sample_function) # Check for examples assert len(doc.examples) == 1 @@ -296,7 +324,35 @@ def raises_func(): """ pass - doc = autodoc.autodoc_function(raises_func) + doc = autodoc_function(raises_func) assert len(doc.raises) == 2 assert any(r.type_name == "ValueError" for r in doc.raises) assert any(r.type_name == "TypeError" for r in doc.raises) + + +def test_render_autodoc(): + """Test rendering of autodoc output.""" + def render(**kwargs): + return f"AUTODOC FOR {kwargs['ds'].name}\n" + + source = """ +``` +::: api tests.test_autodoc.sample_function +::: +``` + +::: api tests.test_autodoc.sample_function +::: +""" + + result = render_autodoc(source, render=render) + print(result) + assert result.strip() == """ +``` +::: api tests.test_autodoc.sample_function +::: +``` + +AUTODOC FOR sample_function +""".strip() + diff --git a/uv.lock b/uv.lock index e77c9bc..162c360 100644 --- a/uv.lock +++ b/uv.lock @@ -4,20 +4,20 @@ requires-python = ">=3.12" [[package]] name = "asttokens" -version = "3.0.0" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "cachetools" -version = "6.2.0" +version = "6.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" }, ] [[package]] @@ -38,6 +38,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -67,41 +141,41 @@ wheels = [ [[package]] name = "executing" -version = "2.2.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "filelock" -version = "3.19.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] name = "hecto" -version = "2.0.1" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/0e/035340c471120bb3375d9dea4549e3944d3f4ce962637ebc3d551ef911b6/hecto-2.0.1.tar.gz", hash = "sha256:8a0f17fd60af786f768070ba0a431493c5870c36a2f252ce77a04537717c67c2", size = 9837, upload-time = "2025-08-31T13:55:19.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/e0/21ef23d8449a44fbb8889a0c716642183441370e97929dd4b64b0615ebb6/hecto-2.2.1.tar.gz", hash = "sha256:a1cddaadc715669ecdb7739a7aa992c267e5d4f7965909fa7de02a9524ee10bf", size = 9551, upload-time = "2025-12-07T21:41:51.642Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/42/274a87c90bd25587daeeaf6d321386b4eb00af7db76a49cc289c390fd809/hecto-2.0.1-py3-none-any.whl", hash = "sha256:9a4e5c69b2e393d0b1cc9f38ad9dbe1838ae3c387173672629d70437c5c9f364", size = 8437, upload-time = "2025-08-31T13:55:18.865Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/e57a6d3367eea5b36fd21397044171fc3a5050782fe0547ebe36ede076ce/hecto-2.2.1-py3-none-any.whl", hash = "sha256:93ce2cc0a3363d29e7bc615e6ab58ed95407e3bcaa2fcbacdefdc90bf1ce4875", size = 8036, upload-time = "2025-12-07T21:41:50.686Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -119,7 +193,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.4.0" +version = "9.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -133,9 +207,9 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" }, ] [[package]] @@ -176,91 +250,116 @@ wheels = [ [[package]] name = "jx" -version = "0.3.0" +version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/c2/8bfd8267f5b779152a5c1e55818bcd5a800254448b9617316521b5e2468a/jx-0.3.0.tar.gz", hash = "sha256:39bedd89d1729679c3c9e25874161caff72c14f96f690ef07f4d620d4b56be94", size = 25545, upload-time = "2025-10-25T16:57:36.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ac/00603141020d0fe1494617a14fc4987fdaa22a71f17490a4919470f82b12/jx-0.5.1.tar.gz", hash = "sha256:bb869be5fbad889eb2e0e4a6bafe9ef2bb6a821ea0b2a9680b9b739973c8bd59", size = 25981, upload-time = "2025-11-21T23:03:40.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/9d/61629c3c4db2d1ba7e5fae7835847dc4c79948a3651421e43ca3ebf4a68a/jx-0.3.0-py3-none-any.whl", hash = "sha256:3b8f53880d5100d3f480afe9d3aa1ab97b17dc61136361584dfc33436d31be5f", size = 18255, upload-time = "2025-10-25T16:57:34.617Z" }, -] - -[[package]] -name = "markdown" -version = "3.8.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/f1025553eb64bcc48bba42527bb334692de8763e6352aa2b051aaa221a2c/jx-0.5.1-py3-none-any.whl", hash = "sha256:1af0eba80928f783582d8adf8761b2d91998e534a9add27ef96b0996d8428a9d", size = 18258, upload-time = "2025-11-21T23:03:38.847Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.1.7" +version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "parso" -version = "0.8.4" +version = "0.8.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] [[package]] @@ -277,11 +376,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] @@ -295,14 +394,14 @@ wheels = [ [[package]] name = "prompt-toolkit" -version = "3.0.51" +version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] @@ -333,44 +432,45 @@ wheels = [ ] [[package]] -name = "pymdown-extensions" -version = "10.16" +name = "pyproject-api" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, + { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] [[package]] -name = "pyproject-api" -version = "1.9.1" +name = "pytest" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] -name = "pytest" -version = "8.4.1" +name = "pytest-cov" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, + { name = "coverage" }, { name = "pluggy" }, - { name = "pygments" }, + { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -385,55 +485,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - [[package]] name = "ruff" -version = "0.12.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/ce/8d7dbedede481245b489b769d27e2934730791a9a82765cb94566c6e6abd/ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873", size = 5131435, upload-time = "2025-07-17T17:27:19.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/9f/517bc5f61bad205b7f36684ffa5415c013862dee02f55f38a217bdbe7aa4/ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a", size = 10188824, upload-time = "2025-07-17T17:26:31.412Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/691baae5a11fbbde91df01c565c650fd17b0eabed259e8b7563de17c6529/ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442", size = 10884521, upload-time = "2025-07-17T17:26:35.084Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8d/756d780ff4076e6dd035d058fa220345f8c458391f7edfb1c10731eedc75/ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e", size = 10277653, upload-time = "2025-07-17T17:26:37.897Z" }, - { url = "https://files.pythonhosted.org/packages/8d/97/8eeee0f48ece153206dce730fc9e0e0ca54fd7f261bb3d99c0a4343a1892/ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586", size = 10485993, upload-time = "2025-07-17T17:26:40.68Z" }, - { url = "https://files.pythonhosted.org/packages/49/b8/22a43d23a1f68df9b88f952616c8508ea6ce4ed4f15353b8168c48b2d7e7/ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb", size = 10022824, upload-time = "2025-07-17T17:26:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/cd/70/37c234c220366993e8cffcbd6cadbf332bfc848cbd6f45b02bade17e0149/ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c", size = 11524414, upload-time = "2025-07-17T17:26:46.219Z" }, - { url = "https://files.pythonhosted.org/packages/14/77/c30f9964f481b5e0e29dd6a1fae1f769ac3fd468eb76fdd5661936edd262/ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a", size = 12419216, upload-time = "2025-07-17T17:26:48.883Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/af7fe0a4202dce4ef62c5e33fecbed07f0178f5b4dd9c0d2fcff5ab4a47c/ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3", size = 11976756, upload-time = "2025-07-17T17:26:51.754Z" }, - { url = "https://files.pythonhosted.org/packages/09/d1/33fb1fc00e20a939c305dbe2f80df7c28ba9193f7a85470b982815a2dc6a/ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045", size = 11020019, upload-time = "2025-07-17T17:26:54.265Z" }, - { url = "https://files.pythonhosted.org/packages/64/f4/e3cd7f7bda646526f09693e2e02bd83d85fff8a8222c52cf9681c0d30843/ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57", size = 11277890, upload-time = "2025-07-17T17:26:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d0/69a85fb8b94501ff1a4f95b7591505e8983f38823da6941eb5b6badb1e3a/ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184", size = 10348539, upload-time = "2025-07-17T17:26:59.381Z" }, - { url = "https://files.pythonhosted.org/packages/16/a0/91372d1cb1678f7d42d4893b88c252b01ff1dffcad09ae0c51aa2542275f/ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb", size = 10009579, upload-time = "2025-07-17T17:27:02.462Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/c4a833e3114d2cc0f677e58f1df6c3b20f62328dbfa710b87a1636a5e8eb/ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1", size = 10942982, upload-time = "2025-07-17T17:27:05.343Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/ce85e445cf0a5dd8842f2f0c6f0018eedb164a92bdf3eda51984ffd4d989/ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b", size = 11343331, upload-time = "2025-07-17T17:27:08.652Z" }, - { url = "https://files.pythonhosted.org/packages/35/cf/441b7fc58368455233cfb5b77206c849b6dfb48b23de532adcc2e50ccc06/ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93", size = 10267904, upload-time = "2025-07-17T17:27:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7e/20af4a0df5e1299e7368d5ea4350412226afb03d95507faae94c80f00afd/ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a", size = 11209038, upload-time = "2025-07-17T17:27:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -473,7 +548,7 @@ wheels = [ [[package]] name = "tox" -version = "4.29.0" +version = "4.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -486,23 +561,23 @@ dependencies = [ { name = "pyproject-api" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/22/a6e962733036faa8cf778fe065ac301f5bb6986b5adb0d3f965fa8bb2934/tox-4.29.0.tar.gz", hash = "sha256:7b3a2bb43974285110eee35a859f2b3f2e87a24f6e1011d83f466b7c75835bd2", size = 200853, upload-time = "2025-08-29T22:32:03.772Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/cb/f8bb9ea02047cc8c2d5881b4fb332d0ce067e91e5f10925b87176915c718/tox-4.29.0-py3-none-any.whl", hash = "sha256:b914f134176cea74c5e01c29cb4befc8afa4cd38b356c3756eff85832d27b5c0", size = 174731, upload-time = "2025-08-29T22:32:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, ] [[package]] name = "tox-uv" -version = "1.28.0" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/9a/f4b675ebcbd623854129891e87045f80c1d8e91b2957496f1fe6e463f291/tox_uv-1.28.0.tar.gz", hash = "sha256:a06ff909f73232b2b7965de19090d887b12b44e44eb0843b2c07266d2957ade2", size = 23265, upload-time = "2025-08-14T17:53:07.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/ac/b32555d190c4440b8d2779d4a19439e5fbd5a3950f7e5a17ead7c7d30cad/tox_uv-1.28.0-py3-none-any.whl", hash = "sha256:3fbe13fa6eb6961df5512e63fc4a5cc0c8d264872674ee09164649f441839053", size = 17225, upload-time = "2025-08-14T17:53:06.299Z" }, + { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, ] [[package]] @@ -516,67 +591,66 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/ba/abedc672a4d706241106923595d68573e995f85aced13aa3ef2e6d5069cf/ty-0.0.1a15.tar.gz", hash = "sha256:b601eb50e981bd3fb857eb17b473cad3728dab67f53370b6790dfc342797eb20", size = 3886937, upload-time = "2025-07-18T13:02:20.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/86/4846900f8b7f3dc7c2ec4e0bbd6bc4a4797f27443d3c9878ece5dfcb1446/ty-0.0.1a15-py3-none-linux_armv6l.whl", hash = "sha256:6110b5afee7ae1b0c8d00770eef4937ed0b700b823da04db04486bc661dc0f80", size = 7807444, upload-time = "2025-07-18T13:01:47.81Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bd/b4ee15ffbf0fda9853aefb6cdfaa8d15a07af6ab1c6c874f7ad9adcdc2bd/ty-0.0.1a15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:855401e2fc1d4376f007ef7684dd9173e6a408adc2bc4610013f40c2a1d68d0f", size = 7913908, upload-time = "2025-07-18T13:01:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5e/c37942782de2ed347ea24227fab61ad80383cee7f339af2be65a7732c4a9/ty-0.0.1a15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a20b21ea9683d92d541de4a534b68b4b595c2d04bf77be0ebfe05c9768ef47e7", size = 7526774, upload-time = "2025-07-18T13:01:52.403Z" }, - { url = "https://files.pythonhosted.org/packages/a4/11/8fa1eba381f2bc70eb8eccb2f93aa6f674b9578a1281cdf4984100de8009/ty-0.0.1a15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7648b0931177233e31d952723f068f2925696e464c436ed8bd820b775053474b", size = 7648872, upload-time = "2025-07-18T13:01:54.166Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/b12e34103638089848d58bb4a2813e9e77969fa7b4479212c9a263e7a176/ty-0.0.1a15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9c6b70ae331585984b79a4574f28619d5ff755515b93b5454d04f5c521ca864", size = 7647674, upload-time = "2025-07-18T13:01:56.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b9/c1d8c8e268fe46a65e77b8a61ef5e76ebf6ce5eec2beeb6a063ab23042fb/ty-0.0.1a15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38cae5d28b2882e66f4786825e87d500cfbb806c30bbcac745f20e459cf92482", size = 8470612, upload-time = "2025-07-18T13:01:57.526Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/c4a246026dbdd9f537d882aa51fa34e3a43288b493952724f71a59fb93cc/ty-0.0.1a15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1a8de6d3185afbf7cc199932d7fc508887e7ddad95a15c930efc4b5445eae6de", size = 8928257, upload-time = "2025-07-18T13:01:59.705Z" }, - { url = "https://files.pythonhosted.org/packages/e1/53/7958aa2a730fea926f992cd217f33363c9d0dd0cb688a7c9afa5d083863e/ty-0.0.1a15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5a38db0c2ceb2f0c20241ef6a1a5b0996dad45532bb50661faf46f28b64b9f0", size = 8576435, upload-time = "2025-07-18T13:02:01.535Z" }, - { url = "https://files.pythonhosted.org/packages/e7/77/6b65b83e28d162951e72212f31a1f9fdf7d30023a37702cb35d451df9fb8/ty-0.0.1a15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0045fe7905813296fa821dad4aaabbe0f011ce34915fdfabf651db5b5f7b9d72", size = 8411987, upload-time = "2025-07-18T13:02:03.394Z" }, - { url = "https://files.pythonhosted.org/packages/40/2f/c58c08165edb2e13b5c10f81fa2fc3f9c576992e7abb2c56d636245a49f6/ty-0.0.1a15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32deff2b8b05e8a8b8bf0f48ca1eef72ec299b9cc546ef9aba7185a033de28b1", size = 8211299, upload-time = "2025-07-18T13:02:05.662Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0b/959d4186d87fc99af7c0cb1c425d351d7204d4ed54638925c21915c338ba/ty-0.0.1a15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:60c330a9f37b260ebdf7d3e7e05ec483fab15116f11317ffd76b0e09598038b0", size = 7550119, upload-time = "2025-07-18T13:02:07.804Z" }, - { url = "https://files.pythonhosted.org/packages/89/08/28b33a1125128f57b09a71d043e6ee06502c773ef0fab03fb54bd58dcfa4/ty-0.0.1a15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:745355c15574d50229c3644e47bad1192e261faaf3a11870641b4902a8d9d8fe", size = 7672278, upload-time = "2025-07-18T13:02:09.339Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ba/5569b0a1a90d302e0636718a73a7c3d7029cfa03670f6cc716a4ab318709/ty-0.0.1a15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:07d53cb7c9c322be41dc79c373024422f6c6cd9e96f658e4b1b3289fe6130274", size = 8092872, upload-time = "2025-07-18T13:02:10.871Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f2/a6e94b8b0189af49e871210b7244c4d49c5ac9cc1167f16dd0f28e026745/ty-0.0.1a15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26b28ed6e6ea80766fdd2608ea6e4daeb211e8de2b4b88376f574667bb90f489", size = 8278734, upload-time = "2025-07-18T13:02:13.059Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ae/90d6008d3afe0762d089b5b363be62c3e19d9730c0b04f823448a56aa5fa/ty-0.0.1a15-py3-none-win32.whl", hash = "sha256:42f8d40aa30ef0c2187b70528151e740b74db47eb84a568fbc636c7294a1046e", size = 7390797, upload-time = "2025-07-18T13:02:14.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/14/fc292587c6e85e0b2584562c2cee26ece7c86e0679a690de86f53ad367bf/ty-0.0.1a15-py3-none-win_amd64.whl", hash = "sha256:2563111b072ea132443629a5fe0ec0cefed94c610cc694fc1bd2f48e179ca966", size = 7978840, upload-time = "2025-07-18T13:02:16.74Z" }, - { url = "https://files.pythonhosted.org/packages/24/e9/4d8c22801c7348ce79456c9c071914d94783c5f575ddddb30161b98a7c34/ty-0.0.1a15-py3-none-win_arm64.whl", hash = "sha256:9ea13096dda97437284b61915da92384d283cd096dbe730a3f63ee644721d2d5", size = 7561340, upload-time = "2025-07-18T13:02:18.629Z" }, +version = "0.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" }, + { url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" }, + { url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" }, ] [[package]] name = "uv" -version = "0.8.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/b0/c3bc06ba5f6b72ba3ad278e854292d81b7aaaea2b6988e40fdb892f813f8/uv-0.8.14.tar.gz", hash = "sha256:7c68e0cde3d048500c073696881c07c2bd97503fc77d7091e1454d3fd58febb4", size = 3543853, upload-time = "2025-08-28T21:55:59.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/a3/bf0a80a7770f5c11a735345073fdf085a031ecd0525ae229ceb3ed7496f5/uv-0.8.14-py3-none-linux_armv6l.whl", hash = "sha256:bae6621a72e6643f140c4e62f10d3a52d210ccdec48bf4f733e6a25d5739e533", size = 18810682, upload-time = "2025-08-28T21:55:07.027Z" }, - { url = "https://files.pythonhosted.org/packages/61/de/e8d3c1669edb70ae165ad6c06598ff237ddbc1dc743cc590a2c30c245b93/uv-0.8.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2334945ef3dba395067164c7e25b0c1420d8fdab9637d33cb753b5dbe0499b2c", size = 18939300, upload-time = "2025-08-28T21:55:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/9e4c3382f79cef69229f4f301ce1b391121f5a9d1015dd82487e08f0d718/uv-0.8.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a65096847d3341713be92e98cb35d5315d172690032405e8ae4e1b0c366a19a", size = 17555624, upload-time = "2025-08-28T21:55:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/03/6d/5200cba528844e33586fadae78c06c054774e7702063356795f6cc124331/uv-0.8.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f7a5d72e4fefae57f675cf0ac0adb9e68fb638f3f95be142b7f072fc6fddfe3e", size = 18151749, upload-time = "2025-08-28T21:55:16.904Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b6/6f9407a792f0ca566b61276cadbffa032cff4039847ac77c47959151f753/uv-0.8.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:935b602d40f0c6a41337de81a02850d6892b0c8c6b5d98543fa229d5bb247364", size = 18472626, upload-time = "2025-08-28T21:55:19.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/a2/2eadfccb1d6aa3672c947071b18c50cee41bdb9c9dba6d8af011a5c44e50/uv-0.8.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34286de8d1244f06124c5bd7b4bfb5ef5791c147e0aa4473c7856c02fedc58ff", size = 19292728, upload-time = "2025-08-28T21:55:22.441Z" }, - { url = "https://files.pythonhosted.org/packages/b6/db/96071cddd37e4bfc9bd10c4daab0942c3d610da92f32c74de07621990455/uv-0.8.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d26ea49a595992bc58d31bb6a10660a8015d902b6845c8ceed1e011866013593", size = 20577332, upload-time = "2025-08-28T21:55:25.774Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4c/8e0da19b4bd5612bd782a82a1869c71e8ea059b59c547230146d36583a39/uv-0.8.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aa721841812e9a74cad883dbd0f6cf908309cc40a86ab33d3576a8b369595a9", size = 20317704, upload-time = "2025-08-28T21:55:28.537Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f2/4ad6abe850e31663d3971eb4af4a3b6ef216870f4f2115ae65e72917ea02/uv-0.8.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5088fa0ceff698a3fb2464f5cd7ebb4af59aa85db4ba83150d4c3af027251228", size = 19615504, upload-time = "2025-08-28T21:55:31.695Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6c/b86f5f2f5aeebb0028034ea180399af23c8cbc42748bba0672c9cabdde38/uv-0.8.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3853202f4eb0bedbe31b0b62b1323521e97306f44f8f4b6ed4bb13b636797873", size = 19605107, upload-time = "2025-08-28T21:55:34.33Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/7b019c63d26d296bf6dfd8ad9b86e51f84b2ec7f37d68f8b93138a3fa404/uv-0.8.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e45047a89592a5b38c88caa6da5d1b70a05c9762ff1c5100f9700f85f533dc99", size = 18412515, upload-time = "2025-08-28T21:55:37.185Z" }, - { url = "https://files.pythonhosted.org/packages/59/b8/c277b6ff1e4fc6d2c4f000ebccef9c2879603875ab092390f7073b911bdf/uv-0.8.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72971573f21e617267b3737750cdb8a9ae99862b06d23df7fde60fc9f8ef78d6", size = 19290057, upload-time = "2025-08-28T21:55:39.769Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/59f84ea996bc3bf52c88bc7ba2d988bc5edfd7d0a9aee7cc0500f77d83ce/uv-0.8.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:ab22d9712f6b06b04359cfaf625722a81fcd0f2335868738dbee26a79a93bd99", size = 18433918, upload-time = "2025-08-28T21:55:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2c/8a76455ea1f578fab8a88457c4d50c28928860335d3420956b75661f5e7b/uv-0.8.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b5003c30c44065b70e03f083d73af45c094f1f96d9c394acafd8f547c2aee4d0", size = 18800856, upload-time = "2025-08-28T21:55:44.697Z" }, - { url = "https://files.pythonhosted.org/packages/f7/87/16699c592d816325554702d771024fbe5ec39127bfbc06d5cb54843673bb/uv-0.8.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:dacfad1193c7facd3a414cc2f3468b4a79a07c565c776a3136f97527a628b960", size = 19704752, upload-time = "2025-08-28T21:55:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e9/0cdeed22e6c540db493ea364040b17af09fabaa7a56c8ff02b9152819442/uv-0.8.14-py3-none-win32.whl", hash = "sha256:0a4abb2a327e3709ef02765dc392ee10e204275bdb107b492977f88633a1e6b0", size = 18630132, upload-time = "2025-08-28T21:55:51.988Z" }, - { url = "https://files.pythonhosted.org/packages/45/5e/9bf7004bd53e9279265d73a131fe2a6c7d74c1125c53e805b5e9f4047f37/uv-0.8.14-py3-none-win_amd64.whl", hash = "sha256:5091d588753bbbd1f120f13311ede2ae113d7ec2760e149fc502a237f2516075", size = 20672637, upload-time = "2025-08-28T21:55:55.341Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7f/41074c81faa36a34d44524997c345a857bd82d7f73ea60e24dca606306ec/uv-0.8.14-py3-none-win_arm64.whl", hash = "sha256:7c424fd4561f4528d8b52fc8c16991d0ad0000d3ad12c82e01e722f314b2669d", size = 19171656, upload-time = "2025-08-28T21:55:57.799Z" }, +version = "0.9.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/7d/005ab1cab03ca928cef75b424284d14d62c5f18775cf8114a63f210a0c9c/uv-0.9.28.tar.gz", hash = "sha256:253c04b26fb40f74c56ead12ce83db3c018bdefde1fcd1a542bcb88fdca4189c", size = 3834456, upload-time = "2026-01-29T20:15:49.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/dc/e70698756f1bb74c88bf1eaea63a114a580a38f296ea1567a01db9007490/uv-0.9.28-py3-none-linux_armv6l.whl", hash = "sha256:aede961243bb2c0ca09d0e04ea0bf580d7128dd3b14661b79d133be9a5b69894", size = 22040477, upload-time = "2026-01-29T20:16:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ed/77294752bf722e1d6b666bd6592b6ac975dabcf1fde49e98a75cac23d45c/uv-0.9.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fe9aa2822d24f6ecec035a06dfdd1fbed570ed40b83a864e71714bad37ddfd3", size = 21025194, upload-time = "2026-01-29T20:15:36.504Z" }, + { url = "https://files.pythonhosted.org/packages/b1/a9/78f2da6217c1bbae3371d68515fe747e1160bab049d6898a03e517802573/uv-0.9.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58a36bf623c6d36b3d60d3c76eeb7275199d607938786e927d40ce213980059d", size = 19783994, upload-time = "2026-01-29T20:16:19.451Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/55639c444e91b96c81c326d39a0a06551d2e611be0cc917b89010ba9ba88/uv-0.9.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4d479a1d387b1464ad2c1f960b0b26a9ac1dfba67ea2c6789e9643fe6d1e7b9a", size = 21568230, upload-time = "2026-01-29T20:15:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/95d7992c0a39981cfbcf56ff8f069c09e0567feb0e70cb8b52bc8a2947a0/uv-0.9.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:29eefd4642f55954a2b9a40619cde3d02856300f59b8cf63ed1a161ca0ca9b77", size = 21633679, upload-time = "2026-01-29T20:15:52.363Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/b6778e03714b1f9da095c8bf0f8e5007f4867d9196c1ae8053504ddf2877/uv-0.9.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4155496f624deb753f5ddd80fbe3797587c8480d1250e83c9fd816b4b02e3a41", size = 21632238, upload-time = "2026-01-29T20:15:55.003Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/0db6ea9fd8f2752a8723a637e3ed881eb212516665ccb2e8066bbea62a52/uv-0.9.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dc98e2d6db0dc9a2f65ce4cda6a34283fa80f3fbfff129befdf40ad7a3d1615", size = 22779474, upload-time = "2026-01-29T20:15:33.513Z" }, + { url = "https://files.pythonhosted.org/packages/54/88/ef70e04113393f4e19e67281cae9f83c82030d14eb4eb811bda83fcd8f44/uv-0.9.28-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d267280b3878aa6ef8e00bff1f11bf61580d0a8bbb69fa95b5d3526d00f77485", size = 24124596, upload-time = "2026-01-29T20:16:05.062Z" }, + { url = "https://files.pythonhosted.org/packages/81/07/9fda9149bc57e79bde5f00cabcef323a68817c1cca9d44e2aa08d18c6b52/uv-0.9.28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba2a320ff77996468789f4b2c573fd766f9330717c440335af8790043b2b3703", size = 23655701, upload-time = "2026-01-29T20:16:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1f1e910ca1a0aca0d0ede3ba0eaca867fd3c575f44b2fe103a5c9511f071/uv-0.9.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8fd93c5bee89ed88908215f81a3baa0d2a98e35caf995b97e9c226c1c29340", size = 22856456, upload-time = "2026-01-29T20:16:16.582Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fd/82561751105ed232f1781747bc336b20e8d57ee07b4d2ed3fa6cf2718d71/uv-0.9.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b8460a2b624d8ab27cb293a2c9f2393f9efc4e36e0fb886a6c2360e23fb48be", size = 22685296, upload-time = "2026-01-29T20:16:13.857Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e4/b905daff0bfde347c49b9c9ba31d09d504c4b84f2749a07db77a9da16dba/uv-0.9.28-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3798c486ec627bbd7ca41fa219e997ad403b1f803371edf5c8e75893e46161ba", size = 21669854, upload-time = "2026-01-29T20:15:30.277Z" }, + { url = "https://files.pythonhosted.org/packages/9a/01/9a90574fe7290c775332e54f163cba58c767445b655e97646708f9c66050/uv-0.9.28-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e479cc5cbfd72ebdbea3c909d0ab997162e0dfa1ee622b50e2f9dc8d07d4eee3", size = 22388944, upload-time = "2026-01-29T20:15:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/cc35014bab3c17b4fe8f6bae84e640ce64d9bb4c8a24694a935e0c0af538/uv-0.9.28-py3-none-musllinux_1_1_i686.whl", hash = "sha256:97d61cdf2436e83a0f188d55d1974e46679d9a787c3a54cb0a40de717c6bf435", size = 22073327, upload-time = "2026-01-29T20:15:58.119Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/e848570be5c5be4e139b90237cc64f68d5d51e8e92c40a5ac7cf0c34ad4a/uv-0.9.28-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cbfa56c833caa37b1f14166327fcaf8aa87290451406921eb07296ffef17fef1", size = 22915580, upload-time = "2026-01-29T20:15:42.468Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/6c3d839ea289bf8509da32f703a47accd63ab409b33627728aebcd2a1b65/uv-0.9.28-py3-none-win32.whl", hash = "sha256:d5cb780d5b821f837f63e7fd14e2bf75f01824b4575a1e89639888771bfd9efd", size = 20856809, upload-time = "2026-01-29T20:15:45.141Z" }, + { url = "https://files.pythonhosted.org/packages/06/a8/d72229dd90d1e5a3c8368d51a70219018d579380945e67c8dcffbe8e53c0/uv-0.9.28-py3-none-win_amd64.whl", hash = "sha256:203ab59710c0c1b3c5ecc684f9cfc9264340a69c8706aaa8aea75415779f0d74", size = 23447461, upload-time = "2026-01-29T20:16:22.563Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/5852eb0c59e5224f4cb0323906efae348f782f8a7f1069197e7cf6ec9b74/uv-0.9.28-py3-none-win_arm64.whl", hash = "sha256:c29406e1dc6b1b312c478c76b42b9f94b684855a4c001901b5488bab6ccf4ec7", size = 21860859, upload-time = "2026-01-29T20:16:00.764Z" }, ] [[package]] name = "virtualenv" -version = "20.34.0" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] @@ -605,32 +679,33 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/3e/3d456efe55d2d5e7938b5f9abd68333dd8dceb14e829f51f9a8deed2217e/wcwidth-0.5.2.tar.gz", hash = "sha256:c022c39a02a0134d1e10810da36d1f984c79648181efcc70a389f4569695f5ae", size = 152817, upload-time = "2026-01-29T19:32:52.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/72/da5a6f511a8267f962a08637464a70409736ac72f9f75b069e0e96d69b64/wcwidth-0.5.2-py3-none-any.whl", hash = "sha256:46912178a64217749bf3426b21e36e849fbc46e05c949407b3e364d9f7ffcadf", size = 90088, upload-time = "2026-01-29T19:32:50.592Z" }, ] [[package]] name = "writeadoc" -version = "0.9.1" +version = "0.10.0" source = { virtual = "." } dependencies = [ { name = "docstring-parser" }, { name = "hecto" }, { name = "jinja2" }, { name = "jx" }, - { name = "markdown" }, + { name = "mistune" }, { name = "pygments" }, - { name = "pymdown-extensions" }, { name = "strictyaml" }, + { name = "ty" }, { name = "watchdog" }, ] [package.dev-dependencies] dev = [ { name = "ipdb" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "tox-uv" }, { name = "ty" }, @@ -645,16 +720,17 @@ requires-dist = [ { name = "hecto", specifier = ">=2.0.1" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jx", specifier = ">=0.3.0" }, - { name = "markdown", specifier = ">=3.8.2" }, + { name = "mistune", specifier = ">=3.2.0" }, { name = "pygments", specifier = ">=2.19.2" }, - { name = "pymdown-extensions", specifier = ">=10.16" }, { name = "strictyaml", specifier = ">=1.7.3" }, + { name = "ty", specifier = ">=0.0.1a15" }, { name = "watchdog", specifier = ">=6.0.0" }, ] [package.metadata.requires-dev] dev = [ { name = "ipdb", specifier = ">=0.13.13" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.12.4" }, { name = "tox-uv" }, { name = "ty", specifier = ">=0.0.1a15" },