diff --git a/znai-docs/znai/llm.txt b/znai-docs/znai/llm.txt index 9deadeda6..c676d668d 100644 --- a/znai-docs/znai/llm.txt +++ b/znai-docs/znai/llm.txt @@ -146,8 +146,8 @@ answer-link: znai-from-export/introduction/getting-started#command-line ## CLI download Download and unzip -[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.92-SNAPSHOT/znai-dist-1.92-SNAPSHOT-znai.zip). -Add it to your `PATH`. +[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.92/znai-dist-1.92-znai.zip). Add +it to your `PATH`. ## Brew @@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin org.testingisdocumenting.znai znai-maven-plugin - 1.92-SNAPSHOT + 1.92 ``` @@ -830,6 +830,9 @@ Title match during search yields the highest score. For example this current section full title is: *Flow Search Local*. +Queries with special characters like `List.map` or `c++` are additionally matched verbatim: sections containing the +exact text appear first. Wrap a query in quotes, e.g. `"List.map"`, to only match sections containing the exact text. + As your documentation grows, keep checking how easy it is to navigate to a section of your documentation using `/`. Avoid: using generic names in your page titles and page section titles. You should not have dozens of pages called @@ -910,7 +913,7 @@ To define a footnote use To add a reference to the footnote use `[^my-id]` which will result in [^my-id] -[^my-id]: extra content for my footnote goes here potentially including code blocks Constructor +[^my-id]: extra content for my footnote goes here potentially including code blocks Constructor() Note: numeric footnotes are treated as text footnotes, and they will be assigned the auto incremented number in order of appearance. @@ -938,7 +941,7 @@ answer-link: znai-from-export/flow/footnotes#footnotes-list A footnotes list is automatically appended at the end of the page when footnotes are present. Clicking the number in the list navigates back to the reference [^another-note]. -[^another-note]: This is another footnote to demonstrate the list with multiple entries +[^another-note]: This is another footnote to demonstrate the list with multiple entries. To hide the footnotes list, set `hideFootnoteList` to `true` in `meta.json`: @@ -2247,6 +2250,11 @@ score, Integer, another description line with *markdown* support ``` ``` +```api-parameters +firstName, String, description with markdown support +score, Integer, another description line with markdown support +``` + # Snippets :: API Parameters :: Nested answer-link: znai-from-export/snippets/api-parameters#nested @@ -2266,6 +2274,15 @@ roles.description, String, role description Note: when using this approach it is necessary to explicitly define a `root` entry, such as `person` in this example. +```api-parameters +sessionId, Integer, session Id +person, Person, person to login with +person.firstName, String, first name of the person +person.lastName, String, last name of the person +roles, List, list of authorized roles +roles.id, String, role id +roles.description, String, role description +``` Note: if a parameter name actually contains a period ("."), you can prevent this nesting behavior by putting the parameter name in single quotes, e.g. "person.firstName" @@ -2282,6 +2299,11 @@ score, Integer, another description line with *markdown* support ``` ``` +```api-parameters +firstName, String, description with markdown support +score, Integer, another description line with markdown support +``` + # Snippets :: API Parameters :: Collapsing Parameters answer-link: znai-from-export/snippets/api-parameters#collapsing-parameters @@ -2294,6 +2316,10 @@ zipCode, String, zip code ``` ``` +```api-parameters +city, String, city name +zipCode, String, zip code +``` Note: `collapsed` requires `title` to be present @@ -2314,6 +2340,16 @@ zipCode, String, zip code ``` ``` +```api-parameters +firstName, String, description with markdown support +score, Integer, another description line with markdown support +``` + +```api-parameters +city, String, city name +zipCode, String, zip code +``` + # Snippets :: API Parameters :: Size answer-link: znai-from-export/snippets/api-parameters#size @@ -2326,6 +2362,11 @@ score, Integer, another description line with *markdown* support ``` ``` +```api-parameters +firstName, String, description with markdown support +score, Integer, another description line with markdown support +``` + # Snippets :: API Parameters :: Multi-line CSV Description answer-link: znai-from-export/snippets/api-parameters#multi-line-csv-description @@ -2350,6 +2391,10 @@ Specify whether to use gpu ````` ``` +```api-parameters +cores, String, Specify how many cores to allocate for execution execute(cores: 10) +gpu, Boolean, "Specify whether to use gpu execute(cores: 10, gpu: true)" +``` Note: Use larger number of backticks on outside then inside to distinct between `api-parameters` plugin boundaries and nested code blocks @@ -2415,6 +2460,15 @@ Given the above file, use to display it as API Parameters +```api-parameters +sessionId, Integer, session Id +person, Person, person to login with +person.firstName, String, first name of the person +person.lastName, String, last name of the person +roles, List, list of authorized roles +roles.id, String, role id +roles.description, String, role description +``` Note: `description` field in JSON file is treated as Markdown @@ -2431,6 +2485,10 @@ score, Integer, another description line with *markdown* support ``` ``` +```api-parameters +firstName, String, description with markdown support +score, Integer, another description line with markdown support +``` In the example above `customPrefix` is added to each parameter link. @@ -2439,6 +2497,10 @@ answer-link: znai-from-export/snippets/api-parameters#long-parameter-names Znai hard wraps long parameter names to leave more space to description. +```api-parameters +VERY_LONG_PARAMETER_NAME_WITHOUT_SPACES, String, description with markdown support +VERY_LONG_ANOTHER_PARAMETER_NAME_WITHOUT_SPACES, String, another description line with markdown support +``` Use `noWrap: true` to remove hard wrap enforcement @@ -2449,6 +2511,11 @@ VERY_LONG_ANOTHER_PARAMETER_NAME_WITHOUT_SPACES, String, another description lin ``` ``` +```api-parameters +VERY_LONG_PARAMETER_NAME_WITHOUT_SPACES, String, description with markdown support +VERY_LONG_ANOTHER_PARAMETER_NAME_WITHOUT_SPACES, String, another description line with markdown support +``` + # Snippets :: API Parameters :: Wide Mode answer-link: znai-from-export/snippets/api-parameters#wide-mode @@ -2462,6 +2529,11 @@ line with *markdown* support and few moe lines" ``` ``` +```api-parameters +VERY_LONG_PARAMETER_NAME_WITHOUT_SPACES, String, "longer line longer line, description with markdown support" +VERY_LONG_ANOTHER_PARAMETER_NAME_WITHOUT_SPACES, String, "longer line longer line, another description line with markdown support and few moe lines" +``` + # Snippets :: Code References :: Local References answer-link: znai-from-export/snippets/code-references#local-references @@ -3854,6 +3926,10 @@ extract description of methods and convert parameters into [API Parameters](snip :include-doxygen-doc-params: utils::nested::my_func { title: "My Params" } ``` +```api-parameters +first_param, int, description of first param item a item b +second_param, bool, description of second param +``` Head over to [CPP Description Extraction](CPP/description-extraction) to learn more @@ -3867,8 +3943,15 @@ Znai provides plugins to automatically create reference documentation for method ``` +```api-parameters +v1, const T1 &, value to print +v2, const T2 &, value to print +``` - +```api-parameters +T1, , type of the value one to print +T2, , type of the value two to print +``` ```hpp @@ -6410,13 +6493,51 @@ Note: Requires `title` to be set. } ``` +# Visuals :: Iframe :: Collapse +answer-link: znai-from-export/visuals/iframe#collapse + +Use `collapsed: true|false` to make iframe collapsible. + +Note: Requires `title` to be set. + +```markdown +:include-iframe: iframe/custom.html { + title: "collapsible content", + fit: true, + collapsed: true +} +``` + +# Visuals :: Iframe :: No Gap +answer-link: znai-from-export/visuals/iframe#no-gap + +Use `noGap: true` to remove top/bottom margins when there are multiple iframes in a row. Use `noGapBorder: true` to add +a delimiter between them: + +```markdown +:include-iframe: iframe/custom.html { + title: "part one", + fit: true, + noGap: true, + noGapBorder: true +} + +:include-iframe: iframe/custom.html { + title: "part two", + fit: true +} +``` + # Visuals :: Iframe :: Embedding Video answer-link: znai-from-export/visuals/iframe#embedding-video Use `include-iframe` to embed media from other places. By default, aspect ratio is set to `16:9`. -``` -:include-iframe: https://www.youtube.com/embed/tgbNymZ7vqY +```markdown +:include-iframe: https://www.youtube.com/embed/tgbNymZ7vqY { + title: "sample video", + newTabEnabled: true +} ``` # Visuals :: Iframe :: Aspect Ratio @@ -8280,9 +8401,18 @@ Use `include-doxygen-doc-params` to extract parameters description from doxygen :include-doxygen-doc-params: utils::nested::my_func { title: "My Params" } ``` +```api-parameters +first_param, int, description of first param item a item b +second_param, bool, description of second param +``` Use `small: true` option to make parameters smaller +```api-parameters +first_param, int, description of first param item a item b +second_param, bool, description of second param +``` + # CPP :: Description Extraction :: Extract Parameters By Args answer-link: znai-from-export/CPP/description-extraction#extract-parameters-by-args @@ -8292,6 +8422,10 @@ Use `args` to select a specific overload by providing parameters string :include-doxygen-doc-params: utils::nested::my_func { title: "My Params", args: "int" } ``` +```api-parameters +first_param, int, description of first param item a item b +``` + # CPP :: Description Extraction :: Extract Template Parameters answer-link: znai-from-export/CPP/description-extraction#extract-template-parameters @@ -8315,6 +8449,11 @@ void multi_println(const T1& v1, const T2& v2) { :include-doxygen-doc-params: multi_println { title: "Template parameters", type: "template" } ``` +```api-parameters +T1, , type of the value one to print +T2, , type of the value two to print +``` + # CPP :: Description Extraction :: Ignore Template Parameters answer-link: znai-from-export/CPP/description-extraction#ignore-template-parameters @@ -8360,6 +8499,12 @@ int add(int a, int b); :include-doxygen-doc-params: math::add ``` +```api-parameters +return, , sum of two numbers +a, int, number to add to +b, int, number that is added +``` + # CPP :: Auto Reference :: Member Signature, Comment And Parameters answer-link: znai-from-export/CPP/auto-reference#member-signature-comment-and-parameters @@ -8405,7 +8550,10 @@ void free_func() { ``` - +```api-parameters +first_param, int, description of first param item a item b +second_param, bool, description of second param +``` ```markdown @@ -8426,6 +8574,17 @@ void multi_println(const T1& v1, const T2& v2) { } ``` + +```api-parameters +v1, const T1 &, value to print +v2, const T2 &, value to print +``` + +```api-parameters +T1, , type of the value one to print +T2, , type of the value two to print +``` + # CPP :: Auto Reference :: Member Signature Only answer-link: znai-from-export/CPP/auto-reference#member-signature-only @@ -8508,7 +8667,10 @@ Use `args` to select a specific overload by providing parameters string ``` - +```api-parameters +param1, long, description of param1 +param3, bool, description of param3 +``` Note: Args are coma and spaces sensitive. Znai will print available args variants in case of mismatch for you to copy @@ -8539,19 +8701,28 @@ Use `include-doxygen-compound` to render a definition of a class/struct #### sing - +```api-parameters +tone, int, tone at which to sing +volume, int, volume at which to sing +``` #### bark - +```api-parameters +myClass, const MyClass &, to test reference +tone, int, tone at which to sing +volume, int, volume at which to sing +``` #### smile - +```api-parameters +volume, int, smile volume +``` #### number_of_sounds @@ -8564,6 +8735,12 @@ Use `include-doxygen-compound` to render a definition of a class/struct #### help + +```api-parameters +tone, int, tone at which to sing +volume, int, volume at which to sing +``` + # CPP :: Auto Reference :: Cross-Reference answer-link: znai-from-export/CPP/auto-reference#cross-reference diff --git a/znai-docs/znai/release-notes/1.93/add-2026-08-31-checkboxes-interactive-checklist.md b/znai-docs/znai/release-notes/1.93/add-2026-08-31-checkboxes-interactive-checklist.md new file mode 100644 index 000000000..42c752fb1 --- /dev/null +++ b/znai-docs/znai/release-notes/1.93/add-2026-08-31-checkboxes-interactive-checklist.md @@ -0,0 +1 @@ +* Add: [checkboxes](visuals/checkboxes) fence plugin renders bullet points as an interactive checklist, checked state persists in browser local storage diff --git a/znai-docs/znai/release-notes/2026.md b/znai-docs/znai/release-notes/2026.md index 524f4030d..7950dea5d 100644 --- a/znai-docs/znai/release-notes/2026.md +++ b/znai-docs/znai/release-notes/2026.md @@ -1,3 +1,7 @@ +# 1.93 + +:include-markdowns: 1.93 + # 1.92 :include-markdowns: 1.92 diff --git a/znai-docs/znai/toc b/znai-docs/znai/toc index 1876ece96..e6635fedd 100644 --- a/znai-docs/znai/toc +++ b/znai-docs/znai/toc @@ -42,6 +42,7 @@ visuals images image-annotations cards + checkboxes charts mermaid-diagrams SVG diff --git a/znai-docs/znai/visuals/checkboxes.md b/znai-docs/znai/visuals/checkboxes.md new file mode 100644 index 000000000..014d3c015 --- /dev/null +++ b/znai-docs/znai/visuals/checkboxes.md @@ -0,0 +1,62 @@ +# Interactive Checklist + +To render an interactive checklist use the `checkboxes` fence plugin. +Each bullet point inside becomes a checkbox users can tick. + + ```checkboxes + * install cli + * setup environment + * run first example + ``` + +```checkboxes +* install cli +* setup environment +* run first example +``` + +Note: Checked state is persisted in the browser local storage, so it survives page reloads and revisits. +Changing checklist content resets its checked state. During local preview state is not persisted. + +# Complex Content + +Bullet points can contain any markdown, including multiple paragraphs and code snippets. + + ````checkboxes + * install dependencies using package manager + + ```bash + npm install --save-dev my-tool + ``` + + * validate generated config + + ```javascript + module.exports = { + preset: "default", + }; + ``` + + * commit changes + ```` + +````checkboxes +* install dependencies using package manager + + ```bash + npm install --save-dev my-tool + ``` + +* validate generated config + + ```javascript + module.exports = { + preset: "default", + }; + ``` + +* commit changes +```` + +Note: Four backticks were used instead of the usual three. This is done so one fence block can include +another block (code snippet) without confusion. It can be any number of backticks greater than three. diff --git a/znai-reactjs/src/App.css b/znai-reactjs/src/App.css index 572d06247..1482f3b04 100644 --- a/znai-reactjs/src/App.css +++ b/znai-reactjs/src/App.css @@ -82,6 +82,9 @@ --znai-card-shadow: 0 2px 2px 0 rgb(0 0 0 / 15%), 0 3px 1px -2px rgb(0 0 0 / 13%), 0 1px 5px 0 rgb(0 0 0 / 22%); --znai-card-link-separator-color: var(--znai-snippets-outer-border-color); + --znai-checkboxes-border-color: #b0b0b0; + --znai-checkboxes-checked-color: var(--znai-brand-primary-color); + --znai-space-above-next-prev-navigation: 150px; --znai-space-below-next-prev-navigation: 100px; --znai-space-left-next-prev-navigation: 50px; diff --git a/znai-reactjs/src/App.jsx b/znai-reactjs/src/App.jsx index 0bbe24cc2..d2fc37498 100644 --- a/znai-reactjs/src/App.jsx +++ b/znai-reactjs/src/App.jsx @@ -101,6 +101,7 @@ import { attentionBlockDemo } from "./doc-elements/paragraph/AttentionBlock.demo import { attentionBlockPresentationDemo } from "./doc-elements/paragraph/PresentationAttentionBlock.demo"; import { containerTitleDemo } from "./doc-elements/container/ContainerTitle.demo"; import { cardsDemo } from "./doc-elements/card/CardsDemo"; +import { checkboxesDemo } from "./doc-elements/checkboxes/Checkboxes.demo"; import { doxygenPresentationDemo } from "./doc-elements/doxygen/PresentationDoxygen.demo"; import { jsonPresentationDemo } from "./doc-elements/json/PresentationJson.demo"; import { footnoteDemo } from "./doc-elements/footnote/Footnote.demo"; @@ -190,6 +191,7 @@ registries .registerAsGrid("SVG", 0, svgDemo) .registerAsGrid("GraphViz SVG", 0, graphVizSvgDemo) .registerAsRows("Cards", cardsDemo) + .registerAsRows("Checkboxes", checkboxesDemo) .registerAsRows("Diagram Legend", diagramLegendDemo) .registerAsGrid("Keyboard shortcuts", 0, keyboardShortcutsDemo) .registerAsGrid("Iframe", 0, iframeDemo) diff --git a/znai-reactjs/src/doc-elements/DefaultElementsLibrary.jsx b/znai-reactjs/src/doc-elements/DefaultElementsLibrary.jsx index 8df429fd1..2b157bcd5 100644 --- a/znai-reactjs/src/doc-elements/DefaultElementsLibrary.jsx +++ b/znai-reactjs/src/doc-elements/DefaultElementsLibrary.jsx @@ -89,6 +89,7 @@ import { PythonMethod } from "./python/PythonMethod"; import { ApiLinkedTextBlock } from "./api/ApiLinkedTextBlock"; import { OpenApiMethodAndUrl } from "./open-api/OpenApiMethodAndUrl"; import { Card } from "./card/Card"; +import { Checkboxes } from "./checkboxes/Checkboxes"; import { FootnoteReference } from "./footnote/FootnoteReference"; import { FootnoteBackLinks } from "./footnote/FootnotesList"; import { EmbeddedHtml } from "./html/EmbeddedHtml"; @@ -183,6 +184,7 @@ presentationElementHandlers.AnnotatedImage = presentationAnnotatedImageHandler library.AnnotatedImageWithOrderedList = AnnotatedImageWithOrderedList library.Card = Card; +library.Checkboxes = Checkboxes; library.FootnoteReference = FootnoteReference; library.FootnoteBackLinks = FootnoteBackLinks; diff --git a/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.css b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.css new file mode 100644 index 000000000..689873f77 --- /dev/null +++ b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.css @@ -0,0 +1,77 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.znai-checkboxes { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 15px; +} + +.znai-checkboxes-item { + display: flex; + align-items: flex-start; + gap: 10px; +} + +/* 1lh keeps the checkbox centered against the first line of the item content */ +.znai-checkboxes-tick-cell { + display: flex; + align-items: center; + height: 1lh; + flex-shrink: 0; +} + +.znai-checkboxes-tick { + appearance: none; + width: 16px; + height: 16px; + margin: 0; + border: 2px solid var(--znai-checkboxes-border-color); + border-radius: 4px; + background-color: transparent; + cursor: pointer; +} + +.znai-checkboxes-tick:hover { + border-color: var(--znai-checkboxes-checked-color); +} + +.znai-checkboxes-tick:focus-visible { + outline: 2px solid var(--znai-checkboxes-checked-color); + outline-offset: 2px; +} + +.znai-checkboxes-tick:checked { + border-color: var(--znai-checkboxes-checked-color); + background-color: var(--znai-checkboxes-checked-color); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E"); + background-size: 12px 12px; + background-position: center; + background-repeat: no-repeat; +} + +.znai-checkboxes-item-content { + flex: 1; + min-width: 0; + transition: opacity 0.15s ease-in-out; + + --znai-single-column-render-width: auto; +} + +.znai-checkboxes-item.checked .znai-checkboxes-item-content { + opacity: 0.55; +} diff --git a/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.demo.tsx b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.demo.tsx new file mode 100644 index 000000000..e109cbe68 --- /dev/null +++ b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.demo.tsx @@ -0,0 +1,112 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; + +import { Registry } from "react-component-viewer"; +import { elementsLibrary } from "../DefaultElementsLibrary"; +import { Checkboxes } from "./Checkboxes"; + +export function checkboxesDemo(registry: Registry) { + registry + .add("one liners", () => ( + + )) + .add("long wrapped text", () => ( + + )) + .add("with code snippet", () => ( + + )); +} + +function oneLiners() { + return [ + { + id: "install-cli", + content: [paragraph("install cli")], + }, + { + id: "setup-environment", + content: [paragraph("setup environment")], + }, + { + id: "run-first-example", + content: [paragraph("run first example")], + }, + ]; +} + +function longText() { + const longLine = + "review the deployment guide and make sure all the required services are provisioned, " + + "credentials are stored in the secrets manager, and the monitoring dashboards are configured " + + "for every environment including staging and production"; + + return [ + { + id: "review-deployment-guide", + content: [paragraph(longLine)], + }, + { + id: "short-follow-up", + content: [paragraph("short follow up")], + }, + ]; +} + +function withSnippet() { + return [ + { + id: "install-dependencies", + content: [ + paragraph("install dependencies using package manager"), + { + type: "Snippet", + lang: "bash", + snippet: "npm install --save-dev my-tool\nnpm run my-tool -- --init\n", + }, + ], + }, + { + id: "validate-config", + content: [ + paragraph("validate generated config"), + { + type: "Snippet", + lang: "javascript", + snippet: 'module.exports = {\n preset: "default",\n output: "./build",\n};\n', + }, + ], + }, + { + id: "commit-changes", + content: [paragraph("commit changes")], + }, + ]; +} + +function paragraph(text: string) { + return { + type: "Paragraph", + content: [ + { + text, + type: "SimpleText", + }, + ], + }; +} diff --git a/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.tsx b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.tsx new file mode 100644 index 000000000..80d46397d --- /dev/null +++ b/znai-reactjs/src/doc-elements/checkboxes/Checkboxes.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from "react"; + +import { DocElementContent, WithElementsLibrary } from "../default-elements/DocElement"; +import { currentPageIdWithDocId } from "../../structure/DocumentationNavigation"; +import { isPreviewEnabled } from "../../structure/docMeta"; + +import "./Checkboxes.css"; + +interface CheckboxItem { + id: string; + content: DocElementContent; +} + +interface Props extends WithElementsLibrary { + blockId: string; + checkboxItems: CheckboxItem[]; +} + +export function Checkboxes({ blockId, checkboxItems, elementsLibrary }: Props) { + const [checkedById, setCheckedById] = useState>(() => loadCheckedState(blockId)); + + return ( +
+ {checkboxItems.map((item) => { + const isChecked = !!checkedById[item.id]; + const itemClassName = "znai-checkboxes-item" + (isChecked ? " checked" : ""); + + return ( +
+
+ toggle(item.id)} + /> +
+
+ +
+
+ ); + })} +
+ ); + + function toggle(id: string) { + const newCheckedById = { ...checkedById, [id]: !checkedById[id] }; + setCheckedById(newCheckedById); + saveCheckedState(blockId, newCheckedById); + } +} + +// blockId is derived from the block content, so each block owns its storage entry +// and editing a checklist resets its state +function storageKey(blockId: string) { + return "znai-checkboxes-state:" + currentPageIdWithDocId() + ":" + blockId; +} + +function loadCheckedState(blockId: string): Record { + // content changes often during preview, no point in persisting state across reloads + if (isPreviewEnabled()) { + return {}; + } + + try { + const stored = localStorage.getItem(storageKey(blockId)); + if (stored) { + const parsed = JSON.parse(stored); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed; + } + } + } catch (e) { + console.warn("failed to load checkboxes state", e); + } + + return {}; +} + +function saveCheckedState(blockId: string, checkedById: Record) { + if (isPreviewEnabled()) { + return; + } + + try { + localStorage.setItem(storageKey(blockId), JSON.stringify(checkedById)); + } catch (e) { + console.warn("failed to save checkboxes state", e); + } +} diff --git a/znai-reactjs/src/theme/znai-dark/znai-dark.css b/znai-reactjs/src/theme/znai-dark/znai-dark.css index 705b1dae0..63b3f0256 100644 --- a/znai-reactjs/src/theme/znai-dark/znai-dark.css +++ b/znai-reactjs/src/theme/znai-dark/znai-dark.css @@ -55,6 +55,8 @@ --znai-card-shadow: 0 2px 2px 0 rgb(0 0 0 / 74%), 0 3px 1px -2px rgb(0 0 0 / 72%), 0 0 5px 0 rgb(0 0 0 / 80%); --znai-card-link-separator-color: #141515; + --znai-checkboxes-border-color: #555; + --znai-toc-title-color: #a0a1a0; --znai-toc-title-background-color: #272f33; --znai-toc-panel-background-color: #272f33; diff --git a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePlugin.java b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePlugin.java new file mode 100644 index 000000000..26e7c6662 --- /dev/null +++ b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePlugin.java @@ -0,0 +1,157 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.testingisdocumenting.znai.extensions.checkboxes; + +import org.testingisdocumenting.znai.core.AuxiliaryFile; +import org.testingisdocumenting.znai.core.ComponentsRegistry; +import org.testingisdocumenting.znai.extensions.PluginParams; +import org.testingisdocumenting.znai.extensions.PluginResult; +import org.testingisdocumenting.znai.extensions.fence.FencePlugin; +import org.testingisdocumenting.znai.parser.MarkupParserResult; +import org.testingisdocumenting.znai.parser.docelement.DocElement; +import org.testingisdocumenting.znai.parser.docelement.DocElementType; +import org.testingisdocumenting.znai.search.SearchScore; +import org.testingisdocumenting.znai.search.SearchText; +import org.testingisdocumenting.znai.utils.NameUtils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class CheckboxesFencePlugin implements FencePlugin { + private static final int MAX_ID_LENGTH = 48; + + private MarkupParserResult contentParseResult; + + @Override + public String id() { + return "checkboxes"; + } + + @Override + public FencePlugin create() { + return new CheckboxesFencePlugin(); + } + + @Override + public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPath, PluginParams pluginParams, String content) { + contentParseResult = componentsRegistry.defaultParser().parse(markupPath, content); + + List> checkboxItems = buildItems(); + if (checkboxItems.isEmpty()) { + throw new IllegalArgumentException("no bullet points found inside checkboxes fence block"); + } + + Map props = new LinkedHashMap<>(); + props.put("blockId", blockIdFromContent(content)); + props.put("checkboxItems", checkboxItems); + + return PluginResult.docElement("Checkboxes", props); + } + + /** + * block identity is derived from the content, so blocks with the same item texts don't share + * persisted checked state, and editing a checklist resets its state + */ + private static String blockIdFromContent(String content) { + try { + byte[] hash = MessageDigest.getInstance("SHA-256").digest(content.trim().getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash, 0, 8); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public Stream auxiliaryFiles(ComponentsRegistry componentsRegistry) { + return contentParseResult.auxiliaryFiles().stream(); + } + + @Override + public List textForSearch() { + return List.of(SearchScore.STANDARD.text(contentParseResult.getAllText())); + } + + private List> buildItems() { + List> items = new ArrayList<>(); + Set usedIds = new HashSet<>(); + + for (DocElement element : contentParseResult.docElement().getContent()) { + if (!element.getType().equals(DocElementType.BULLET_LIST)) { + throw new IllegalArgumentException("only bullet points are supported inside checkboxes fence block, " + + "found: " + element.getType()); + } + + for (DocElement listItem : element.getContent()) { + Map item = new LinkedHashMap<>(); + item.put("id", buildItemId(listItem, usedIds)); + item.put("content", listItem.contentToListOfMaps()); + + items.add(item); + } + } + + return items; + } + + private String buildItemId(DocElement listItem, Set usedIds) { + StringBuilder text = new StringBuilder(); + collectText(listItem, text); + + String id = NameUtils.idFromTitle(text.toString().trim()); + if (id.length() > MAX_ID_LENGTH) { + id = id.substring(0, MAX_ID_LENGTH); + } + + if (id.isEmpty()) { + id = "item"; + } + + String result = id; + int suffix = 2; + while (!usedIds.add(result)) { + result = id + "-" + suffix++; + } + + return result; + } + + private void collectText(DocElement element, StringBuilder result) { + Object text = element.getProp("text"); + if (text == null) { + text = element.getProp("code"); + } + + if (text != null) { + if (!result.isEmpty()) { + result.append(' '); + } + result.append(text); + } + + element.getContent().forEach(child -> collectText(child, result)); + } +} diff --git a/znai-website-gen/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin b/znai-website-gen/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin index 75cf1ac71..dfc821d09 100644 --- a/znai-website-gen/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin +++ b/znai-website-gen/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin @@ -22,4 +22,5 @@ org.testingisdocumenting.znai.extensions.markup.MarkdownAndResultFencePlugin org.testingisdocumenting.znai.extensions.table.TableFencePlugin org.testingisdocumenting.znai.extensions.cli.CliFencePlugin org.testingisdocumenting.znai.extensions.image.ImageFencePlugin -org.testingisdocumenting.znai.extensions.card.CardFencePlugin \ No newline at end of file +org.testingisdocumenting.znai.extensions.card.CardFencePlugin +org.testingisdocumenting.znai.extensions.checkboxes.CheckboxesFencePlugin \ No newline at end of file diff --git a/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePluginTest.groovy b/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePluginTest.groovy new file mode 100644 index 000000000..6e83ac4c4 --- /dev/null +++ b/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/extensions/checkboxes/CheckboxesFencePluginTest.groovy @@ -0,0 +1,113 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.testingisdocumenting.znai.extensions.checkboxes + +import org.junit.Test +import org.testingisdocumenting.znai.extensions.PluginParamsFactory +import org.testingisdocumenting.znai.parser.TestComponentsRegistry +import org.testingisdocumenting.znai.parser.commonmark.MarkdownParser + +import java.nio.file.Paths + +import static org.testingisdocumenting.webtau.Matchers.code +import static org.testingisdocumenting.webtau.Matchers.throwException +import static org.testingisdocumenting.znai.parser.TestComponentsRegistry.TEST_COMPONENTS_REGISTRY + +class CheckboxesFencePluginTest { + static PluginParamsFactory pluginParamsFactory = TEST_COMPONENTS_REGISTRY.pluginParamsFactory() + + @Test + void "build checkbox items from bullet points"() { + def props = process("* buy milk\n" + + "* write docs\n") + + props.checkboxItems.should == [ + [id: 'buy-milk', content: [[type: 'Paragraph', content: [[text: 'buy milk', type: 'SimpleText']]]]], + [id: 'write-docs', content: [[type: 'Paragraph', content: [[text: 'write docs', type: 'SimpleText']]]]]] + } + + @Test + void "generate unique ids for items with the same text"() { + def props = process("* buy milk\n" + + "* buy milk\n") + + props.checkboxItems.id.should == ['buy-milk', 'buy-milk-2'] + } + + @Test + void "derive block id from content so blocks with same item text have distinct identity"() { + def props = process("* buy milk\n") + def samePropsAgain = process("* buy milk\n") + def differentProps = process("* buy milk\n* write docs\n") + + props.blockId.should == samePropsAgain.blockId + props.blockId.shouldNot == differentProps.blockId + } + + @Test + void "include inline code text in generated ids"() { + def props = process("* install `cli`\n") + + props.checkboxItems.id.should == ['install-cli'] + } + + @Test + void "preserve complex content of a bullet point"() { + def props = process("* buy milk\n" + + "\n" + + " extra paragraph with details\n") + + props.checkboxItems[0].content.type.should == ['Paragraph', 'Paragraph'] + } + + @Test + void "validate only bullet points are present"() { + code { + process("regular paragraph text\n") + } should throwException("only bullet points are supported inside checkboxes fence block, found: Paragraph") + } + + @Test + void "validate at least one bullet point is present"() { + code { + process("") + } should throwException("no bullet points found inside checkboxes fence block") + } + + @Test + void "indexes text of all items"() { + def pluginAndProps = processAndGetPluginWithProps("* buy milk\n" + + "* write docs\n") + + pluginAndProps.plugin.textForSearch().text.should == ['buy milk write docs'] + } + + private static Map process(String markup) { + return processAndGetPluginWithProps(markup).props + } + + private static Map processAndGetPluginWithProps(String markup) { + def componentsRegistry = new TestComponentsRegistry() + componentsRegistry.defaultParser = new MarkdownParser(componentsRegistry) + + def plugin = new CheckboxesFencePlugin() + def result = plugin.process(componentsRegistry, Paths.get("test.md"), + pluginParamsFactory.create(plugin.id(), ""), markup) + + return [plugin: plugin, props: result.docElements[0].toMap()] + } +}