From a270d634f233d911362e3233f61bc803fb0bbb8c Mon Sep 17 00:00:00 2001
From: Shibo Lyu <hi@lao.sb>
Date: Sun, 16 Feb 2025 22:44:37 +0800
Subject: [PATCH] feat: richText

---
 deno.json                    |  2 +-
 richText/mod.ts              | 33 +++++++++++++++++++++++++++++++++
 richText/richText.test.ts    |  8 ++++++++
 richText/richText.ts         |  5 +++++
 richText/span.test.ts        |  8 ++++++++
 richText/span.ts             | 27 +++++++++++++++++++++++++++
 richText/toPlainText.test.ts |  8 ++++++++
 richText/toPlainText.ts      |  6 ++++++
 scripts/build_npm.ts         |  1 +
 9 files changed, 97 insertions(+), 1 deletion(-)
 create mode 100644 richText/mod.ts
 create mode 100644 richText/richText.test.ts
 create mode 100644 richText/richText.ts
 create mode 100644 richText/span.test.ts
 create mode 100644 richText/span.ts
 create mode 100644 richText/toPlainText.test.ts
 create mode 100644 richText/toPlainText.ts

diff --git a/deno.json b/deno.json
index 223b95e..edae569 100644
--- a/deno.json
+++ b/deno.json
@@ -15,7 +15,7 @@
     "build:npm": "deno run -A ./scripts/build_npm.ts"
   },
   "publish": {
-    "include": ["LICENSE", "README.md", "crypto", "identity"],
+    "include": ["LICENSE", "README.md", "crypto", "identity", "richText"],
     "exclude": ["**/*.test.ts"]
   },
   "test": {
diff --git a/richText/mod.ts b/richText/mod.ts
new file mode 100644
index 0000000..6c9f234
--- /dev/null
+++ b/richText/mod.ts
@@ -0,0 +1,33 @@
+/**
+ * `richText` defines the structure of rich text used through out Blah.
+ *
+ * Note that this module only defines a single block of rich text. That is, we only deal with "inline" elements.
+ *
+ * @module
+ */
+
+import type z from "zod";
+
+import {
+  type BlahRichTextSpan,
+  type BlahRichTextSpanAttributes,
+  blahRichTextSpanAttributesSchema as internalBlahRichTextSpanAttributesSchema,
+  blahRichTextSpanSchema as internalBlahRichTextSpanSchema,
+} from "./span.ts";
+const blahRichTextSpanAttributesSchema: z.ZodType<BlahRichTextSpanAttributes> =
+  internalBlahRichTextSpanAttributesSchema;
+const blahRichTextSpanSchema: z.ZodType<BlahRichTextSpan> =
+  internalBlahRichTextSpanSchema;
+export {
+  type BlahRichTextSpan,
+  type BlahRichTextSpanAttributes,
+  blahRichTextSpanAttributesSchema,
+  blahRichTextSpanSchema,
+};
+
+import {
+  type BlahRichText,
+  blahRichTextSchema as internalBlahRichTextSchema,
+} from "./richText.ts";
+const blahRichTextSchema: z.ZodType<BlahRichText> = internalBlahRichTextSchema;
+export { type BlahRichText, blahRichTextSchema };
diff --git a/richText/richText.test.ts b/richText/richText.test.ts
new file mode 100644
index 0000000..057be2d
--- /dev/null
+++ b/richText/richText.test.ts
@@ -0,0 +1,8 @@
+import { assertTypeMatchesZodSchema } from "../test/utils.ts";
+import { type BlahRichText, blahRichTextSchema } from "./richText.ts";
+
+Deno.test("type BlahRichText is accurate", () => {
+  assertTypeMatchesZodSchema<BlahRichText>(
+    blahRichTextSchema,
+  );
+});
diff --git a/richText/richText.ts b/richText/richText.ts
new file mode 100644
index 0000000..516ffd3
--- /dev/null
+++ b/richText/richText.ts
@@ -0,0 +1,5 @@
+import { z } from "zod";
+import { type BlahRichTextSpan, blahRichTextSpanSchema } from "./span.ts";
+
+export const blahRichTextSchema = z.array(blahRichTextSpanSchema);
+export type BlahRichText = Array<BlahRichTextSpan>;
diff --git a/richText/span.test.ts b/richText/span.test.ts
new file mode 100644
index 0000000..63cb390
--- /dev/null
+++ b/richText/span.test.ts
@@ -0,0 +1,8 @@
+import { assertTypeMatchesZodSchema } from "../test/utils.ts";
+import { type BlahRichTextSpan, blahRichTextSpanSchema } from "./span.ts";
+
+Deno.test("type BlahRichTextSpan is accurate", () => {
+  assertTypeMatchesZodSchema<BlahRichTextSpan>(
+    blahRichTextSpanSchema,
+  );
+});
diff --git a/richText/span.ts b/richText/span.ts
new file mode 100644
index 0000000..8090984
--- /dev/null
+++ b/richText/span.ts
@@ -0,0 +1,27 @@
+import { z } from "zod";
+
+export const blahRichTextSpanAttributesSchema = z.object({
+  b: z.boolean().default(false),
+  i: z.boolean().default(false),
+  u: z.boolean().default(false),
+  s: z.boolean().default(false),
+  m: z.boolean().default(false),
+  tag: z.boolean().default(false),
+  link: z.string().url().optional(),
+});
+
+export type BlahRichTextSpanAttributes = {
+  b?: boolean;
+  i?: boolean;
+  u?: boolean;
+  s?: boolean;
+  m?: boolean;
+  tag?: boolean;
+  link?: string | undefined;
+};
+
+export const blahRichTextSpanSchema = z.union([
+  z.string(),
+  z.tuple([z.string(), blahRichTextSpanAttributesSchema]),
+]);
+export type BlahRichTextSpan = string | [string, BlahRichTextSpanAttributes];
diff --git a/richText/toPlainText.test.ts b/richText/toPlainText.test.ts
new file mode 100644
index 0000000..73045b3
--- /dev/null
+++ b/richText/toPlainText.test.ts
@@ -0,0 +1,8 @@
+import { expect } from "@std/expect";
+import type { BlahRichText } from "./mod.ts";
+import { toPlainText } from "./toPlainText.ts";
+
+Deno.test("toPlainText", () => {
+  const richText: BlahRichText = ["hello ", ["world", { b: true }]];
+  expect(toPlainText(richText)).toBe("hello world");
+});
diff --git a/richText/toPlainText.ts b/richText/toPlainText.ts
new file mode 100644
index 0000000..e197d02
--- /dev/null
+++ b/richText/toPlainText.ts
@@ -0,0 +1,6 @@
+import type { BlahRichText } from "./richText.ts";
+
+export function toPlainText(richText: BlahRichText): string {
+  return richText.map((span) => (typeof span === "string" ? span : span[0]))
+    .join("");
+}
diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts
index 69c7c2f..81a8541 100644
--- a/scripts/build_npm.ts
+++ b/scripts/build_npm.ts
@@ -7,6 +7,7 @@ await build({
   entryPoints: [
     { name: "./crypto", path: "crypto/mod.ts" },
     { name: "./identity", path: "identity/mod.ts" },
+    { name: "./richText", path: "richText/mod.ts" },
   ],
   outDir: "./npm",
   importMap: "deno.json",