Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.attention;

import org.testingisdocumenting.znai.extensions.PluginParamType;
import org.testingisdocumenting.znai.extensions.PluginParams;
import org.testingisdocumenting.znai.extensions.PluginParamsDefinition;
import org.testingisdocumenting.znai.extensions.fence.FencePlugin;

public class AttentionSignCustomFencePlugin extends AttentionSignFencePluginBase {
@Override
protected String type() {
return "custom";
}

@Override
public PluginParamsDefinition parameters() {
return super.parameters()
.add("icon", PluginParamType.STRING, "optional icon id to display next to the content, " +
"no icon is used when not specified", "\"info\"");
}

@Override
protected String attentionType(PluginParams pluginParams) {
String customType = pluginParams.getFreeParam();
if (customType == null || customType.isBlank()) {
throw new IllegalArgumentException("attention-custom requires a type as the first free form parameter, " +
"e.g. ```attention-custom my-type");
}

return customType.trim();
}

@Override
public FencePlugin create() {
return new AttentionSignCustomFencePlugin();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public String id() {

abstract protected String type();

protected String attentionType(PluginParams pluginParams) {
return type();
}

@Override
public PluginParamsDefinition parameters() {
return new PluginParamsDefinition()
Expand All @@ -55,7 +59,7 @@ public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPa
parserResult = markupParser.parse(markupPath, content);

Map<String, Object> props = pluginParams.getOpts().toMap();
props.put("attentionType", type());
props.put("attentionType", attentionType(pluginParams));
props.put("content", parserResult.docElement().contentToListOfMaps());

return PluginResult.docElement("AttentionBlock", props);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ org.testingisdocumenting.znai.extensions.attention.AttentionSignQuestionFencePlu
org.testingisdocumenting.znai.extensions.attention.AttentionSignWarningFencePlugin
org.testingisdocumenting.znai.extensions.attention.AttentionSignAvoidFencePlugin
org.testingisdocumenting.znai.extensions.attention.AttentionSignRecommendationFencePlugin
org.testingisdocumenting.znai.extensions.attention.AttentionSignCustomFencePlugin
org.testingisdocumenting.znai.extensions.json.JsonFencePlugin
org.testingisdocumenting.znai.extensions.latex.LatexFencePlugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.attention

import org.junit.Test
import org.testingisdocumenting.znai.extensions.PluginParamsFactory
import org.testingisdocumenting.znai.extensions.include.PluginsTestUtils

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 AttentionSignCustomFencePluginTest {
static PluginParamsFactory pluginParamsFactory = TEST_COMPONENTS_REGISTRY.pluginParamsFactory()

@Test
void "uses free form parameter as the attention type"() {
def props = process("my-type", [:], "hello world")
props.attentionType.should == "my-type"
}

@Test
void "trims the free form type"() {
def props = process(" my-type ", [:], "hello world")
props.attentionType.should == "my-type"
}

@Test
void "supports optional label"() {
def props = process("my-type", [label: "Consider"], "hello world")
props.attentionType.should == "my-type"
props.label.should == "Consider"
}

@Test
void "supports optional icon"() {
def props = process("my-type", [icon: "zap"], "hello world")
props.attentionType.should == "my-type"
props.icon.should == "zap"
}

@Test
void "has no icon by default"() {
def props = process("my-type", [:], "hello world")
props.containsKey("icon").should == false
}

@Test
void "fails when type is not provided"() {
code {
process("", [:], "hello world")
} should throwException(~/attention-custom requires a type/)
}

private static def process(String type, Map<String, ?> params, String content) {
return PluginsTestUtils.processFenceAndGetProps(
pluginParamsFactory.create("attention-custom", type, params), content)
}
}
17 changes: 17 additions & 0 deletions znai-docs/znai/attention-custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.znai-attention-block.my-type {
border-left: 3px solid #6f42c1;
background: #f3effb;
}

.znai-attention-block.my-type .znai-attention-block-icon {
color: #6f42c1;
}

.theme-znai-dark .znai-attention-block.my-type {
border-left-color: #b794f6;
background: rgba(159, 122, 234, 0.12);
}

.theme-znai-dark .znai-attention-block.my-type .znai-attention-block-icon {
color: #b794f6;
}
20 changes: 16 additions & 4 deletions znai-docs/znai/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"cssResources": ["custom.css", "plugins/javascript/theme-box.css", "plugins/javascript/activity-feed.css"],
"jsResources": ["custom.js", "plugins/javascript/theme-box.js", "plugins/javascript/activity-feed.js"],
"cssResources": [
"custom.css",
"attention-custom.css",
"plugins/javascript/theme-box.css",
"plugins/javascript/activity-feed.css"
],
"jsResources": [
"custom.js",
"plugins/javascript/theme-box.js",
"plugins/javascript/activity-feed.js"
],
"htmlResources": ["custom.html"],
"htmlHeadResources": ["tracking.html"],
"plugins": ["plugins/themed-box-plugin.json", "plugins/custom-fence-block-plugin.json"]
}
"plugins": [
"plugins/themed-box-plugin.json",
"plugins/custom-fence-block-plugin.json"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add: [Attention Signs](visuals/attention-signs) `attention-custom` block
50 changes: 50 additions & 0 deletions znai-docs/znai/visuals/attention-signs.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,53 @@ attention-<type>
```attention-recommendation
`recommendation`
```

# Custom Attention Block

Use `attention-custom` when the built-in types are not enough. The free form parameter defines
the type. It is used as a CSS class name, exactly like `note`, `warning`, and the other built-in types.


`````markdown
```attention-custom my-type
hello world
```
`````

```attention-custom my-type
hello world
```

`attention-custom` only provides the markup placeholders. Each guide is responsible for implementing
the CSS for its own types.

Use in combination with `style.css`. Scope rules under `.theme-znai-dark` to define dark mode colors.

:include-file: attention-custom.css {title: "style.css"}

# Custom Icon

Unlike the built-in types, a custom type has no icon by default. Use the `icon` parameter to display one.

To pick an icons to use go to [Feather icons](https://feathericons.com/).
`````markdown
```attention-custom my-type {icon: "zap"}
hello world
```
`````

```attention-custom my-type {icon: "zap"}
hello world
```

Combine `icon` with the optional `label`

`````markdown
```attention-custom my-type {icon: "zap", label: "Consider"}
hello world
```
`````

```attention-custom my-type {icon: "zap", label: "Consider"}
hello world
```
14 changes: 14 additions & 0 deletions znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,18 @@ export function attentionBlockDemo(registry: Registry) {
elementsLibrary={elementsLibrary}
/>
));

registry.add("custom type without icon", () => (
<AttentionBlock attentionType="my-type" content={multipleParagraph} elementsLibrary={elementsLibrary} />
));

registry.add("custom type with icon and label", () => (
<AttentionBlock
attentionType="my-type"
icon="zap"
label="Consider"
content={multipleParagraph}
elementsLibrary={elementsLibrary}
/>
));
}
15 changes: 9 additions & 6 deletions znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import "./AttentionBlock.css";
interface Props extends DocElementProps {
attentionType: string;
label?: string;
icon?: string;
iconTooltip?: string;
content: DocElementContent;
}
Expand All @@ -36,14 +37,16 @@ const iconByType: Record<string, string> = {
recommendation: "check-circle",
};

export function AttentionBlock({ attentionType, label, iconTooltip, content, elementsLibrary }: Props) {
const iconId = iconByType[attentionType] || "square";
export function AttentionBlock({ attentionType, label, icon, iconTooltip, content, elementsLibrary }: Props) {
const iconId = icon ?? iconByType[attentionType];
return (
<div className={`znai-attention-block ${attentionType} content-block`}>
<span className="znai-attention-block-icon" title={tooltipToUse()}>
<Icon id={iconId} />
{label && <span className="znai-attention-block-label">{label}:</span>}
</span>
{(iconId || label) && (
<span className="znai-attention-block-icon" title={tooltipToUse()}>
{iconId && <Icon id={iconId} />}
{label && <span className="znai-attention-block-label">{label}:</span>}
</span>
)}
<span className="znai-attention-block-content">
<elementsLibrary.DocElement content={content} elementsLibrary={elementsLibrary} />
</span>
Expand Down
Loading