Skip to content

Commit c08502f

Browse files
attention: custom attention blocks (#1545)
1 parent d154b28 commit c08502f

10 files changed

Lines changed: 238 additions & 11 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.znai.extensions.attention;
18+
19+
import org.testingisdocumenting.znai.extensions.PluginParamType;
20+
import org.testingisdocumenting.znai.extensions.PluginParams;
21+
import org.testingisdocumenting.znai.extensions.PluginParamsDefinition;
22+
import org.testingisdocumenting.znai.extensions.fence.FencePlugin;
23+
24+
public class AttentionSignCustomFencePlugin extends AttentionSignFencePluginBase {
25+
@Override
26+
protected String type() {
27+
return "custom";
28+
}
29+
30+
@Override
31+
public PluginParamsDefinition parameters() {
32+
return super.parameters()
33+
.add("icon", PluginParamType.STRING, "optional icon id to display next to the content, " +
34+
"no icon is used when not specified", "\"info\"");
35+
}
36+
37+
@Override
38+
protected String attentionType(PluginParams pluginParams) {
39+
String customType = pluginParams.getFreeParam();
40+
if (customType == null || customType.isBlank()) {
41+
throw new IllegalArgumentException("attention-custom requires a type as the first free form parameter, " +
42+
"e.g. ```attention-custom my-type");
43+
}
44+
45+
return customType.trim();
46+
}
47+
48+
@Override
49+
public FencePlugin create() {
50+
return new AttentionSignCustomFencePlugin();
51+
}
52+
}

znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignFencePluginBase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public String id() {
4343

4444
abstract protected String type();
4545

46+
protected String attentionType(PluginParams pluginParams) {
47+
return type();
48+
}
49+
4650
@Override
4751
public PluginParamsDefinition parameters() {
4852
return new PluginParamsDefinition()
@@ -55,7 +59,7 @@ public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPa
5559
parserResult = markupParser.parse(markupPath, content);
5660

5761
Map<String, Object> props = pluginParams.getOpts().toMap();
58-
props.put("attentionType", type());
62+
props.put("attentionType", attentionType(pluginParams));
5963
props.put("content", parserResult.docElement().contentToListOfMaps());
6064

6165
return PluginResult.docElement("AttentionBlock", props);

znai-core/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ org.testingisdocumenting.znai.extensions.attention.AttentionSignQuestionFencePlu
2222
org.testingisdocumenting.znai.extensions.attention.AttentionSignWarningFencePlugin
2323
org.testingisdocumenting.znai.extensions.attention.AttentionSignAvoidFencePlugin
2424
org.testingisdocumenting.znai.extensions.attention.AttentionSignRecommendationFencePlugin
25+
org.testingisdocumenting.znai.extensions.attention.AttentionSignCustomFencePlugin
2526
org.testingisdocumenting.znai.extensions.json.JsonFencePlugin
2627
org.testingisdocumenting.znai.extensions.latex.LatexFencePlugin
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.znai.extensions.attention
18+
19+
import org.junit.Test
20+
import org.testingisdocumenting.znai.extensions.PluginParamsFactory
21+
import org.testingisdocumenting.znai.extensions.include.PluginsTestUtils
22+
23+
import static org.testingisdocumenting.webtau.Matchers.code
24+
import static org.testingisdocumenting.webtau.Matchers.throwException
25+
import static org.testingisdocumenting.znai.parser.TestComponentsRegistry.TEST_COMPONENTS_REGISTRY
26+
27+
class AttentionSignCustomFencePluginTest {
28+
static PluginParamsFactory pluginParamsFactory = TEST_COMPONENTS_REGISTRY.pluginParamsFactory()
29+
30+
@Test
31+
void "uses free form parameter as the attention type"() {
32+
def props = process("my-type", [:], "hello world")
33+
props.attentionType.should == "my-type"
34+
}
35+
36+
@Test
37+
void "trims the free form type"() {
38+
def props = process(" my-type ", [:], "hello world")
39+
props.attentionType.should == "my-type"
40+
}
41+
42+
@Test
43+
void "supports optional label"() {
44+
def props = process("my-type", [label: "Consider"], "hello world")
45+
props.attentionType.should == "my-type"
46+
props.label.should == "Consider"
47+
}
48+
49+
@Test
50+
void "supports optional icon"() {
51+
def props = process("my-type", [icon: "zap"], "hello world")
52+
props.attentionType.should == "my-type"
53+
props.icon.should == "zap"
54+
}
55+
56+
@Test
57+
void "has no icon by default"() {
58+
def props = process("my-type", [:], "hello world")
59+
props.containsKey("icon").should == false
60+
}
61+
62+
@Test
63+
void "fails when type is not provided"() {
64+
code {
65+
process("", [:], "hello world")
66+
} should throwException(~/attention-custom requires a type/)
67+
}
68+
69+
private static def process(String type, Map<String, ?> params, String content) {
70+
return PluginsTestUtils.processFenceAndGetProps(
71+
pluginParamsFactory.create("attention-custom", type, params), content)
72+
}
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.znai-attention-block.my-type {
2+
border-left: 3px solid #6f42c1;
3+
background: #f3effb;
4+
}
5+
6+
.znai-attention-block.my-type .znai-attention-block-icon {
7+
color: #6f42c1;
8+
}
9+
10+
.theme-znai-dark .znai-attention-block.my-type {
11+
border-left-color: #b794f6;
12+
background: rgba(159, 122, 234, 0.12);
13+
}
14+
15+
.theme-znai-dark .znai-attention-block.my-type .znai-attention-block-icon {
16+
color: #b794f6;
17+
}

znai-docs/znai/extensions.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
{
2-
"cssResources": ["custom.css", "plugins/javascript/theme-box.css", "plugins/javascript/activity-feed.css"],
3-
"jsResources": ["custom.js", "plugins/javascript/theme-box.js", "plugins/javascript/activity-feed.js"],
2+
"cssResources": [
3+
"custom.css",
4+
"attention-custom.css",
5+
"plugins/javascript/theme-box.css",
6+
"plugins/javascript/activity-feed.css"
7+
],
8+
"jsResources": [
9+
"custom.js",
10+
"plugins/javascript/theme-box.js",
11+
"plugins/javascript/activity-feed.js"
12+
],
413
"htmlResources": ["custom.html"],
514
"htmlHeadResources": ["tracking.html"],
6-
"plugins": ["plugins/themed-box-plugin.json", "plugins/custom-fence-block-plugin.json"]
7-
}
15+
"plugins": [
16+
"plugins/themed-box-plugin.json",
17+
"plugins/custom-fence-block-plugin.json"
18+
]
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add: [Attention Signs](visuals/attention-signs) `attention-custom` block

znai-docs/znai/visuals/attention-signs.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,53 @@ attention-<type>
133133
```attention-recommendation
134134
`recommendation`
135135
```
136+
137+
# Custom Attention Block
138+
139+
Use `attention-custom` when the built-in types are not enough. The free form parameter defines
140+
the type. It is used as a CSS class name, exactly like `note`, `warning`, and the other built-in types.
141+
142+
143+
`````markdown
144+
```attention-custom my-type
145+
hello world
146+
```
147+
`````
148+
149+
```attention-custom my-type
150+
hello world
151+
```
152+
153+
`attention-custom` only provides the markup placeholders. Each guide is responsible for implementing
154+
the CSS for its own types.
155+
156+
Use in combination with `style.css`. Scope rules under `.theme-znai-dark` to define dark mode colors.
157+
158+
:include-file: attention-custom.css {title: "style.css"}
159+
160+
# Custom Icon
161+
162+
Unlike the built-in types, a custom type has no icon by default. Use the `icon` parameter to display one.
163+
164+
To pick an icons to use go to [Feather icons](https://feathericons.com/).
165+
`````markdown
166+
```attention-custom my-type {icon: "zap"}
167+
hello world
168+
```
169+
`````
170+
171+
```attention-custom my-type {icon: "zap"}
172+
hello world
173+
```
174+
175+
Combine `icon` with the optional `label`
176+
177+
`````markdown
178+
```attention-custom my-type {icon: "zap", label: "Consider"}
179+
hello world
180+
```
181+
`````
182+
183+
```attention-custom my-type {icon: "zap", label: "Consider"}
184+
hello world
185+
```

znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,18 @@ export function attentionBlockDemo(registry: Registry) {
4848
elementsLibrary={elementsLibrary}
4949
/>
5050
));
51+
52+
registry.add("custom type without icon", () => (
53+
<AttentionBlock attentionType="my-type" content={multipleParagraph} elementsLibrary={elementsLibrary} />
54+
));
55+
56+
registry.add("custom type with icon and label", () => (
57+
<AttentionBlock
58+
attentionType="my-type"
59+
icon="zap"
60+
label="Consider"
61+
content={multipleParagraph}
62+
elementsLibrary={elementsLibrary}
63+
/>
64+
));
5165
}

znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import "./AttentionBlock.css";
2424
interface Props extends DocElementProps {
2525
attentionType: string;
2626
label?: string;
27+
icon?: string;
2728
iconTooltip?: string;
2829
content: DocElementContent;
2930
}
@@ -36,14 +37,16 @@ const iconByType: Record<string, string> = {
3637
recommendation: "check-circle",
3738
};
3839

39-
export function AttentionBlock({ attentionType, label, iconTooltip, content, elementsLibrary }: Props) {
40-
const iconId = iconByType[attentionType] || "square";
40+
export function AttentionBlock({ attentionType, label, icon, iconTooltip, content, elementsLibrary }: Props) {
41+
const iconId = icon ?? iconByType[attentionType];
4142
return (
4243
<div className={`znai-attention-block ${attentionType} content-block`}>
43-
<span className="znai-attention-block-icon" title={tooltipToUse()}>
44-
<Icon id={iconId} />
45-
{label && <span className="znai-attention-block-label">{label}:</span>}
46-
</span>
44+
{(iconId || label) && (
45+
<span className="znai-attention-block-icon" title={tooltipToUse()}>
46+
{iconId && <Icon id={iconId} />}
47+
{label && <span className="znai-attention-block-label">{label}:</span>}
48+
</span>
49+
)}
4750
<span className="znai-attention-block-content">
4851
<elementsLibrary.DocElement content={content} elementsLibrary={elementsLibrary} />
4952
</span>

0 commit comments

Comments
 (0)