Commit 1e114fe

HPCesia <me@hpcesia.com>
2025-06-15 15:43:52
feat(user components): add ruby
1 parent 2beeb7c
Changed files (4)
src/components/user/index.ts
@@ -3,6 +3,7 @@ export { default as Icon } from './Icon.astro';
 export { default as LinkCard } from './LinkCard.astro';
 export { default as Repl } from './Repl.astro';
 export { default as RepoCard } from './RepoCard.astro';
+export { default as Ruby } from './Ruby.astro';
 export { default as Tabs } from './Tabs.astro';
 export { default as TabItem } from './TabItem.astro';
 export { default as Tooltip } from './Tooltip.astro';
src/components/user/Ruby.astro
@@ -0,0 +1,34 @@
+---
+interface RubyPair {
+  base: string;
+  text: string;
+}
+
+type Props = (RubyPair | { pairs: RubyPair[] | [string, string][] }) & {
+  rp?: [string, string];
+};
+
+const props = Astro.props;
+const rpContent = props.rp || ['(', ')'];
+const pairs = (() => {
+  const p = 'pairs' in props ? props.pairs : ([props] as RubyPair[]);
+  if (p.length < 1) return p;
+  if ('base' in p[0]) return p;
+  return (p as [string, string][]).map(([base, text]) => ({ base, text }));
+})() as RubyPair[];
+---
+
+<ruby>
+  {
+    pairs.map(({ base, text }) => {
+      return (
+        <Fragment>
+          {base}
+          <rp>{rpContent[0]}</rp>
+          <rt>{text}</rt>
+          <rp>{rpContent[1]}</rp>
+        </Fragment>
+      );
+    })
+  }
+</ruby>
src/content/posts/components.mdx
@@ -9,12 +9,11 @@ tags:
 published: 2025-02-10T21:23:23+08:00
 ---
 
-import { Collapse, Icon, LinkCard, Repl, RepoCard, TabItem, Tabs, Tooltip } from '@components/user';
+import { Collapse, Icon, LinkCard, Repl, RepoCard, Ruby, TabItem, Tabs, Tooltip } from '@components/user';
 
 Components let you easily reuse a piece of UI or styling consistently. You can use them not just in `.astro` files, but also in `.mdx` files.
 
-> [!TIP]
-> [MDX](https://mdxjs.com/) is a format that lets you write JSX embedded inside Markdown. And it has no difference with markdown files in other ways.
+> [!TIP] > [MDX](https://mdxjs.com/) is a format that lets you write JSX embedded inside Markdown. And it has no difference with markdown files in other ways.
 
 > [!TIP]
 > Some components are both available in `.md` and `.mdx` files, but some are only available in `.mdx` files. In `.md` files, you can use components with [remark-directive](https://github.com/remarkjs/remark-directive). Components that available in `.md` files will have a `md` tab in the example.
@@ -143,6 +142,7 @@ Components let you easily reuse a piece of UI or styling consistently. You can u
 </Repl>
 
 ### LinkCard
+
 <Repl>
   <LinkCard
     title="Astral Halo"
@@ -257,6 +257,34 @@ Components let you easily reuse a piece of UI or styling consistently. You can u
   </Fragment>
 </Repl>
 
+### Ruby
+
+<Repl>
+  <div class="flex flex-col gap-2 w-fit mx-auto">
+    <Ruby pairs={[{base: "汉", text: "hàn"}, {base: "字", text: "zì"}]} />
+    <Ruby pairs={[["漢", "ㄏㄢ"], ["字", "ㄗ"]]} />
+    <Ruby base="漢字" text="からじ" />
+  </div>
+  <Fragment slot="desc">
+  <Tabs>
+    <TabItem label="mdx" active>
+      ```jsx
+      <Ruby pairs={[{base: "汉", text: "hàn"}, {base: "字", text: "zì"}]} />
+      <Ruby pairs={[["漢", "ㄏㄢ"], ["字", "ㄗ"]]} />
+      <Ruby base="漢字" text="からじ" />
+      ```
+    </TabItem>
+    <TabItem label="md">
+      ```md
+      ::rubyc{base="汉|字" text="hàn|zì"}
+      ::rubyc{base="漢|字" text="ㄏㄢ|ㄗ"}
+      ::rubyc{base="漢字" text="からじ"}
+      ```
+    </TabItem>
+  </Tabs>
+  </Fragment>
+</Repl>
+
 ## Web Contents
 
 ### RepoCard
src/plugins/rehype-components-list.ts
@@ -156,6 +156,37 @@ const LinkCard = function (props: { title: string; description: string; url: str
   return h('a', { class: wrapperClassName, href: url, title }, bodyNode);
 };
 
+const Ruby = function (props: { base: string; text: string }) {
+  const pairs = (() => {
+    const { base, text } = props;
+    const pattern = /(?<!\\)\|/g;
+    const baseGroups = base.split(pattern);
+    let textGroups = text.split(pattern);
+    if (baseGroups.length > textGroups.length) {
+      console.warn('[WARN] Invalid ruby, base splitter number should lesser than text.');
+      console.warn(`         base: "${base}"`);
+      console.warn(`         text: "${text}"`);
+      return [{ base, text }];
+    }
+    textGroups[baseGroups.length - 1] = textGroups.slice(baseGroups.length - 1).join(' ');
+    textGroups = textGroups.slice(0, baseGroups.length);
+    return baseGroups.map((b, i) => ({ base: b, text: textGroups[i] }));
+  })();
+  return h(
+    'ruby',
+    {},
+    pairs.flatMap(
+      ({ base, text }) =>
+        [
+          { type: 'text', value: base },
+          h('rp', {}, '('),
+          h('rt', {}, text),
+          h('rp', {}, ')'),
+        ] as Child
+    )
+  );
+};
+
 const Tooltip = function (
   props: {
     tip: string;
@@ -172,5 +203,6 @@ export const rehypeComponentsList = {
   collapse: Collapse,
   icon: Icon,
   linkcard: LinkCard,
+  rubyc: Ruby,
   tooltip: Tooltip,
 };