diff --git a/app/packages/rehype-attention-block/icon-svg-hasts.ts b/app/packages/rehype-attention-block/icon-svg-hasts.ts
new file mode 100644
index 0000000..4b2f618
--- /dev/null
+++ b/app/packages/rehype-attention-block/icon-svg-hasts.ts
@@ -0,0 +1,183 @@
+import type { Element } from "hast";
+
+const NOTE_ICON_HAST: Element = {
+ type: "element",
+ tagName: "svg",
+ properties: {
+ xmlns: "http://www.w3.org/2000/svg",
+ viewBox: "0 0 512 512",
+ },
+ children: [
+ {
+ type: "comment",
+ value:
+ "!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.",
+ position: {
+ start: {
+ line: 1,
+ column: 63,
+ offset: 62,
+ },
+ end: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ },
+ },
+ {
+ type: "element",
+ tagName: "path",
+ properties: {
+ d: "M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z",
+ },
+ children: [],
+ position: {
+ start: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ end: {
+ line: 1,
+ column: 486,
+ offset: 485,
+ },
+ },
+ },
+ ],
+ position: {
+ start: {
+ line: 1,
+ column: 1,
+ offset: 0,
+ },
+ end: {
+ line: 1,
+ column: 492,
+ offset: 491,
+ },
+ },
+};
+
+const WARNING_ICON_HAST: Element = {
+ type: "element",
+ tagName: "svg",
+ properties: {
+ xmlns: "http://www.w3.org/2000/svg",
+ viewBox: "0 0 512 512",
+ },
+ children: [
+ {
+ type: "comment",
+ value:
+ "!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.",
+ position: {
+ start: {
+ line: 1,
+ column: 63,
+ offset: 62,
+ },
+ end: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ },
+ },
+ {
+ type: "element",
+ tagName: "path",
+ properties: {
+ d: "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z",
+ },
+ children: [],
+ position: {
+ start: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ end: {
+ line: 1,
+ column: 537,
+ offset: 536,
+ },
+ },
+ },
+ ],
+ position: {
+ start: {
+ line: 1,
+ column: 1,
+ offset: 0,
+ },
+ end: {
+ line: 1,
+ column: 543,
+ offset: 542,
+ },
+ },
+};
+
+const TIP_ICON_HAST: Element = {
+ type: "element",
+ tagName: "svg",
+ properties: {
+ xmlns: "http://www.w3.org/2000/svg",
+ viewBox: "0 0 448 512",
+ },
+ children: [
+ {
+ type: "comment",
+ value:
+ "!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.",
+ position: {
+ start: {
+ line: 1,
+ column: 63,
+ offset: 62,
+ },
+ end: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ },
+ },
+ {
+ type: "element",
+ tagName: "path",
+ properties: {
+ d: "M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z",
+ },
+ children: [],
+ position: {
+ start: {
+ line: 1,
+ column: 214,
+ offset: 213,
+ },
+ end: {
+ line: 1,
+ column: 621,
+ offset: 620,
+ },
+ },
+ },
+ ],
+ position: {
+ start: {
+ line: 1,
+ column: 1,
+ offset: 0,
+ },
+ end: {
+ line: 1,
+ column: 627,
+ offset: 626,
+ },
+ },
+};
+
+export { NOTE_ICON_HAST, WARNING_ICON_HAST, TIP_ICON_HAST };
diff --git a/app/packages/rehype-attention-block/index.ts b/app/packages/rehype-attention-block/index.ts
new file mode 100644
index 0000000..80f2838
--- /dev/null
+++ b/app/packages/rehype-attention-block/index.ts
@@ -0,0 +1,59 @@
+import type { Element, Root, Text } from "hast";
+import type { Plugin } from "unified";
+import { visit } from "unist-util-visit";
+import { TIP_ICON_HAST, WARNING_ICON_HAST, NOTE_ICON_HAST } from "./icon-svg-hasts";
+
+type BlockType = "note" | "warning" | "tip";
+
+const getSVGFromType = (type: BlockType): Element => {
+ switch (type) {
+ case "note":
+ return NOTE_ICON_HAST;
+ case "warning":
+ return WARNING_ICON_HAST;
+ case "tip":
+ return TIP_ICON_HAST;
+ }
+};
+
+const rehypeAttentionBlock: Plugin<[], Root> = () => {
+ return (tree) => {
+ visit(tree, "element", (node) => {
+ if (
+ node.tagName === "div" &&
+ Array.isArray(node.properties.className) &&
+ node.properties.className.includes("remark-atb-base")
+ ) {
+ const blockType = node.properties["data-block-type"];
+ if (!blockType || typeof blockType !== "string") {
+ return;
+ }
+
+ const svgHast = getSVGFromType(blockType as BlockType);
+ const labelTextHast: Text = {
+ type: "text",
+ value: blockType.toUpperCase(),
+ };
+ const spanHast: Element = {
+ type: "element",
+ tagName: "span",
+ properties: {
+ className: ["remark-atb-label"],
+ },
+ children: [labelTextHast],
+ };
+
+ node.children.unshift({
+ type: "element",
+ tagName: "div",
+ properties: {
+ className: [`remark-atb-icon ${blockType}`],
+ },
+ children: [svgHast, spanHast],
+ });
+ }
+ });
+ };
+};
+
+export { rehypeAttentionBlock };
diff --git a/app/packages/remark-attention-block/index.ts b/app/packages/remark-attention-block/index.ts
new file mode 100644
index 0000000..6e37a73
--- /dev/null
+++ b/app/packages/remark-attention-block/index.ts
@@ -0,0 +1,29 @@
+import type { Root } from "mdast";
+import type { Plugin } from "unified";
+import { visit } from "unist-util-visit";
+
+const remarkAttentionBlock: Plugin<[], Root> = () => {
+ const regex = /\[!(?:WARNING|NOTE|TIP)\]/g;
+ return (tree) => {
+ visit(tree, "blockquote", (node) => {
+ if (node.children.length === 1 && node.children[0].type === "paragraph") {
+ const [paragraph] = node.children;
+ if (paragraph.children.length > 0 && paragraph.children[0].type === "text") {
+ const matches = paragraph.children[0].value.match(regex);
+ if (matches) {
+ const type = matches[0].toLowerCase().slice(2, -1);
+ node.data = {
+ hName: "div",
+ hProperties: {
+ className: ["remark-atb-base", `remark-atb-${type}`],
+ "data-block-type": type,
+ },
+ };
+ paragraph.children = paragraph.children.slice(2);
+ }
+ }
+ }
+ });
+ };
+};
+export { remarkAttentionBlock };
diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx
index 35af0b4..48e181f 100644
--- a/app/routes/_renderer.tsx
+++ b/app/routes/_renderer.tsx
@@ -45,6 +45,7 @@ export default jsxRenderer(({ children, title, description }) => {
{/* global css */}
+
diff --git a/app/routes/posts/md-test.mdx b/app/routes/posts/md-test.mdx
index a0ab816..3f99d0f 100644
--- a/app/routes/posts/md-test.mdx
+++ b/app/routes/posts/md-test.mdx
@@ -40,7 +40,7 @@ publishedAt: 2024/09/14
console.log("Hello, TypeScript!");
```
-- こちらは、Mermaid のサンプルです:ok_hand:
+### (自作) rehype-mermaid のサンプル
```mermaid
classDiagram
@@ -58,3 +58,14 @@ Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
```
+
+### (自作) remark-attention-block / rehype-attention-block のサンプル
+
+> [!NOTE]
+> ソードアート・オンライン, 通称SAOは、Reki Kawaharaによる日本のライトノベルシリーズ。
+
+> [!WARNING]
+> このライブラリは、まだ開発中です。予期せぬエラーが発生する可能性があります。
+
+> [!TIP]
+> 実は、このサイトの製作者は、タピオカを人生で一度も食べたことがありません。(本当:thumbsup:)
diff --git a/public/static/rehype-attention-block.css b/public/static/rehype-attention-block.css
new file mode 100644
index 0000000..dca5f49
--- /dev/null
+++ b/public/static/rehype-attention-block.css
@@ -0,0 +1,57 @@
+.remark-atb-base {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ padding: 12px 24px;
+ margin-top: 24px;
+ border-radius: 6px;
+ font-size: 16px;
+
+ > p {
+ margin: 0;
+ }
+}
+
+.remark-atb-note {
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+}
+
+.note {
+ fill: #155724;
+ color: #155724;
+}
+
+.remark-atb-warning {
+ background-color: #fff3cd;
+ border-color: #ffeeba;
+}
+
+.warning {
+ fill: #856404;
+ color: #856404;
+}
+
+.remark-atb-tip {
+ background-color: #d1ecf1;
+ border-color: #bee5eb;
+}
+
+.tip {
+ fill: #0c5460;
+ color: #0c5460;
+}
+
+.remark-atb-icon {
+ max-width: 36px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+}
+
+.remark-atb-label {
+ font-weight: bold;
+ font-size: 12px;
+}
diff --git a/vite.config.ts b/vite.config.ts
index 2805882..a3ada26 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -12,6 +12,8 @@ import rehypeMomiji from './app/packages/rehype-momiji'
import remarkMomijiCodeFilename from './app/packages/remark-momiji-filename'
import rehypeMermaid from './app/packages/rehype-mermaid/rehypeMermaid'
import remarkEmojiName from './app/packages/remark-emoji-name'
+import { remarkAttentionBlock } from './app/packages/remark-attention-block'
+import { rehypeAttentionBlock } from './app/packages/rehype-attention-block'
export default defineConfig(() => {
return {
@@ -24,8 +26,8 @@ export default defineConfig(() => {
ssg({ entry: "./app/server.ts" }),
mdx({
jsxImportSource: 'hono/jsx',
- remarkPlugins: [remarkGfm, remarkBreaks, remarkFrontmatter, remarkMdxFrontmatter, remarkMomijiCodeFilename, remarkEmojiName],
- rehypePlugins: [[rehypeMomiji, { excludeLangs: ['mermaid'] }], rehypeMermaid],
+ remarkPlugins: [remarkGfm, remarkBreaks, remarkFrontmatter, remarkMdxFrontmatter, remarkMomijiCodeFilename, remarkEmojiName, remarkAttentionBlock],
+ rehypePlugins: [[rehypeMomiji, { excludeLangs: ['mermaid'] }], rehypeMermaid, rehypeAttentionBlock],
})
],
}