@yaebal/rich 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +104 -30
  2. package/lib/blocks.d.ts +93 -32
  3. package/lib/blocks.d.ts.map +1 -1
  4. package/lib/blocks.js +227 -71
  5. package/lib/blocks.js.map +1 -1
  6. package/lib/blocks.test.d.ts +2 -0
  7. package/lib/blocks.test.d.ts.map +1 -0
  8. package/lib/blocks.test.js +142 -0
  9. package/lib/blocks.test.js.map +1 -0
  10. package/lib/document.d.ts +28 -0
  11. package/lib/document.d.ts.map +1 -0
  12. package/lib/document.js +46 -0
  13. package/lib/document.js.map +1 -0
  14. package/lib/draft.d.ts +46 -9
  15. package/lib/draft.d.ts.map +1 -1
  16. package/lib/draft.js +115 -19
  17. package/lib/draft.js.map +1 -1
  18. package/lib/draft.test.d.ts +2 -0
  19. package/lib/draft.test.d.ts.map +1 -0
  20. package/lib/draft.test.js +122 -0
  21. package/lib/draft.test.js.map +1 -0
  22. package/lib/escape.d.ts +8 -0
  23. package/lib/escape.d.ts.map +1 -1
  24. package/lib/escape.js +17 -0
  25. package/lib/escape.js.map +1 -1
  26. package/lib/escape.test.d.ts +2 -0
  27. package/lib/escape.test.d.ts.map +1 -0
  28. package/lib/escape.test.js +28 -0
  29. package/lib/escape.test.js.map +1 -0
  30. package/lib/guards.test.d.ts +2 -0
  31. package/lib/guards.test.d.ts.map +1 -0
  32. package/lib/guards.test.js +85 -0
  33. package/lib/guards.test.js.map +1 -0
  34. package/lib/index.d.ts +18 -9
  35. package/lib/index.d.ts.map +1 -1
  36. package/lib/index.js +17 -8
  37. package/lib/index.js.map +1 -1
  38. package/lib/index.test.js +73 -103
  39. package/lib/index.test.js.map +1 -1
  40. package/lib/inline.d.ts +48 -59
  41. package/lib/inline.d.ts.map +1 -1
  42. package/lib/inline.js +87 -82
  43. package/lib/inline.js.map +1 -1
  44. package/lib/inline.test.d.ts +2 -0
  45. package/lib/inline.test.d.ts.map +1 -0
  46. package/lib/inline.test.js +84 -0
  47. package/lib/inline.test.js.map +1 -0
  48. package/lib/node.d.ts +25 -0
  49. package/lib/node.d.ts.map +1 -0
  50. package/lib/node.js +21 -0
  51. package/lib/node.js.map +1 -0
  52. package/lib/plaintext.test.d.ts +2 -0
  53. package/lib/plaintext.test.d.ts.map +1 -0
  54. package/lib/plaintext.test.js +100 -0
  55. package/lib/plaintext.test.js.map +1 -0
  56. package/lib/render.d.ts +17 -0
  57. package/lib/render.d.ts.map +1 -0
  58. package/lib/render.js +30 -0
  59. package/lib/render.js.map +1 -0
  60. package/lib/send.d.ts +6 -3
  61. package/lib/send.d.ts.map +1 -1
  62. package/lib/send.js +12 -3
  63. package/lib/send.js.map +1 -1
  64. package/lib/template.d.ts +46 -0
  65. package/lib/template.d.ts.map +1 -0
  66. package/lib/template.js +82 -0
  67. package/lib/template.js.map +1 -0
  68. package/lib/template.test.d.ts +2 -0
  69. package/lib/template.test.d.ts.map +1 -0
  70. package/lib/template.test.js +78 -0
  71. package/lib/template.test.js.map +1 -0
  72. package/package.json +6 -5
  73. package/src/blocks.test.ts +233 -0
  74. package/src/blocks.ts +304 -86
  75. package/src/document.ts +51 -0
  76. package/src/draft.test.ts +167 -0
  77. package/src/draft.ts +148 -21
  78. package/src/escape.test.ts +38 -0
  79. package/src/escape.ts +20 -0
  80. package/src/guards.test.ts +138 -0
  81. package/src/index.test.ts +79 -140
  82. package/src/index.ts +26 -11
  83. package/src/inline.test.ts +125 -0
  84. package/src/inline.ts +131 -97
  85. package/src/node.ts +43 -0
  86. package/src/plaintext.test.ts +141 -0
  87. package/src/render.ts +54 -0
  88. package/src/send.ts +17 -10
  89. package/src/template.test.ts +100 -0
  90. package/src/template.ts +115 -0
  91. package/lib/message.d.ts +0 -26
  92. package/lib/message.d.ts.map +0 -1
  93. package/lib/message.js +0 -31
  94. package/lib/message.js.map +0 -1
  95. package/src/message.ts +0 -45
@@ -0,0 +1,100 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { heading, list, paragraph } from "./blocks.js";
4
+ import { RichDocument } from "./document.js";
5
+ import { bold } from "./inline.js";
6
+ import { RichError } from "./node.js";
7
+ import { document, html, md } from "./template.js";
8
+
9
+ test("tagged form escapes interpolated text per dialect and keeps the literal skeleton raw", () => {
10
+ const user = "<script>alert(1)</script>";
11
+
12
+ assert.equal(html`<p>${user}</p>`.content, "<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>");
13
+ assert.equal(md`# ${"a*b"}`.content, "# a\\*b");
14
+ });
15
+
16
+ test("the same builder node renders into whichever dialect the template asks for", () => {
17
+ const status = bold("ok");
18
+
19
+ assert.equal(html`state: ${status}`.content, "state: <b>ok</b>");
20
+ assert.equal(md`state: ${status}`.content, "state: **ok**");
21
+ });
22
+
23
+ test("tagged form dedents the literal skeleton but not interpolated values", () => {
24
+ const doc = md`
25
+ # title
26
+
27
+ ${" indented value"}
28
+ `;
29
+
30
+ assert.equal(doc.content, "# title\n\n indented value");
31
+ });
32
+
33
+ test("array and falsy interpolations — arrays concatenate, null/undefined/false vanish", () => {
34
+ const cond = false as boolean;
35
+
36
+ assert.equal(html`${["a", bold("b")]}${cond && "no"}${null}`.content, "a<b>b</b>");
37
+ });
38
+
39
+ test("a RichDocument interpolates raw when the dialect matches, throws RichError when it doesn't", () => {
40
+ const inner = md`**pre-rendered**`;
41
+
42
+ assert.equal(md`${inner}!`.content, "**pre-rendered**!");
43
+ assert.throws(() => html`${inner}`, RichError);
44
+ });
45
+
46
+ test("plain-function form passes a pre-formatted string through untouched", () => {
47
+ assert.equal(html("<p>as-is</p>").content, "<p>as-is</p>");
48
+ assert.equal(md("# as-is").content, "# as-is");
49
+ });
50
+
51
+ test("block-array form joins blocks — blank lines in markdown, adjacent tags in html", () => {
52
+ const blocks = [heading(1, "t"), paragraph("p"), list(["a"])];
53
+
54
+ assert.equal(html(blocks).content, "<h1>t</h1><p>p</p><ul><li>a</li></ul>");
55
+ assert.equal(md(blocks).content, "# t\n\np\n\n- a");
56
+ });
57
+
58
+ test("templates return a RichDocument that serializes to the right InputRichMessage shape", () => {
59
+ assert.deepEqual(html`x`.toInputRichMessage(), { html: "x" });
60
+ assert.deepEqual(md`x`.toInputRichMessage(), { markdown: "x" });
61
+ });
62
+
63
+ test("RichDocument fluent flags land in the payload, and toJSON() mirrors toInputRichMessage()", () => {
64
+ const doc = html`x`.rtl().noEntityDetection();
65
+
66
+ assert.deepEqual(doc.toInputRichMessage(), {
67
+ html: "x",
68
+ is_rtl: true,
69
+ skip_entity_detection: true,
70
+ });
71
+ assert.deepEqual(JSON.parse(JSON.stringify({ rich_message: doc })), {
72
+ rich_message: { html: "x", is_rtl: true, skip_entity_detection: true },
73
+ });
74
+ });
75
+
76
+ test("RichDocument flags are settable to false explicitly", () => {
77
+ assert.deepEqual(html`x`.rtl(false).toInputRichMessage(), { html: "x", is_rtl: false });
78
+ });
79
+
80
+ test("document() assembles blocks with options — html by default, markdown on request", () => {
81
+ assert.deepEqual(document([paragraph("hi")]).toInputRichMessage(), { html: "<p>hi</p>" });
82
+ assert.deepEqual(
83
+ document([paragraph("hi")], { dialect: "markdown", rtl: true }).toInputRichMessage(),
84
+ {
85
+ markdown: "hi",
86
+ is_rtl: true,
87
+ },
88
+ );
89
+ assert.deepEqual(document(paragraph("hi"), { skipEntityDetection: true }).toInputRichMessage(), {
90
+ html: "<p>hi</p>",
91
+ skip_entity_detection: true,
92
+ });
93
+ });
94
+
95
+ test("document() result is a RichDocument, so the fluent setters chain onto it", () => {
96
+ const doc = document([paragraph("hi")]);
97
+
98
+ assert.ok(doc instanceof RichDocument);
99
+ assert.deepEqual(doc.rtl().toInputRichMessage(), { html: "<p>hi</p>", is_rtl: true });
100
+ });
@@ -0,0 +1,115 @@
1
+ import { RichDocument } from "./document.js";
2
+ import type { Dialect } from "./node.js";
3
+ import { type Insertable, render } from "./render.js";
4
+
5
+ // strip the common leading indentation shared by the literal skeleton, then trim blank edges.
6
+ function dedent(skeleton: string): string {
7
+ const rawLines = skeleton.split("\n");
8
+ let min = Number.POSITIVE_INFINITY;
9
+
10
+ for (const line of rawLines) {
11
+ if (line.trim() === "") continue;
12
+ const indent = /^[ \t]*/.exec(line)?.[0].length ?? 0;
13
+ min = Math.min(min, indent);
14
+ }
15
+
16
+ if (!Number.isFinite(min)) min = 0;
17
+
18
+ return rawLines
19
+ .map((line) => line.slice(min))
20
+ .join("\n")
21
+ .replace(/^\n+/, "")
22
+ .trimEnd();
23
+ }
24
+
25
+ export interface RichTemplate {
26
+ /** plain-function form: passed through as-is, no escaping/dedent (already-formatted content). */
27
+ (source: string): RichDocument;
28
+ /** block-array form: each entry rendered; blocks blank-line-joined in markdown. */
29
+ (blocks: readonly Insertable[]): RichDocument;
30
+ /** tagged-template form: dedented, `${}` interpolation escaped/rendered per-item. */
31
+ (strings: TemplateStringsArray, ...subs: Insertable[]): RichDocument;
32
+ }
33
+
34
+ // top-level blocks need a blank line between them in markdown; in html the tags
35
+ // carry the structure and separators are just cosmetic whitespace.
36
+ function joinBlocks(blocks: readonly Insertable[], dialect: Dialect): string {
37
+ const separator = dialect === "markdown" ? "\n\n" : "";
38
+ return blocks.map((block) => render(block, dialect)).join(separator);
39
+ }
40
+
41
+ function makeTemplate(dialect: Dialect): RichTemplate {
42
+ return ((
43
+ first: string | TemplateStringsArray | readonly Insertable[],
44
+ ...subs: Insertable[]
45
+ ): RichDocument => {
46
+ if (typeof first === "string") return new RichDocument(dialect, first);
47
+
48
+ // a plain array (no `.raw` template marker) composes top-level blocks.
49
+ if (!("raw" in first)) {
50
+ return new RichDocument(dialect, joinBlocks(first as readonly Insertable[], dialect));
51
+ }
52
+
53
+ // dedent the literal skeleton only (\x00 marks interpolation seams so values aren't dedented).
54
+ const seam = String.fromCharCode(0);
55
+ const dedented = dedent((first as TemplateStringsArray).join(seam)).split(seam);
56
+
57
+ let out = "";
58
+ for (let i = 0; i < dedented.length; i++) {
59
+ out += dedented[i] ?? "";
60
+ if (i < subs.length) out += render(subs[i] as Insertable, dialect);
61
+ }
62
+
63
+ return new RichDocument(dialect, out);
64
+ }) as RichTemplate;
65
+ }
66
+
67
+ /**
68
+ * the html authoring entry point — a tagged template whose interpolations are
69
+ * auto-escaped (never re-parsed as markup); builder nodes render themselves,
70
+ * a nested `RichDocument` is spliced in raw (dialect-checked). also accepts a
71
+ * block array (`html([heading(1, t), table(rows)])`) or a plain pre-formatted
72
+ * string. returns a `RichDocument` — pass it straight to `sendRichMessage`.
73
+ */
74
+ export const html: RichTemplate = makeTemplate("html");
75
+
76
+ /**
77
+ * the markdown authoring entry point — same three forms as `html`, emitting
78
+ * `InputRichMessage.markdown` instead. the *same* builders work under both tags:
79
+ * every `RichNode` renders itself into whichever dialect the template asks for.
80
+ *
81
+ * @example
82
+ * md`
83
+ * # ${title}
84
+ *
85
+ * ${bold("status:")} ${status}
86
+ * `
87
+ */
88
+ export const md: RichTemplate = makeTemplate("markdown");
89
+
90
+ export interface DocumentOptions {
91
+ /** target dialect. defaults to `"html"` (the fully documented one). */
92
+ dialect?: Dialect;
93
+ /** `InputRichMessage.is_rtl` — show the message right-to-left. */
94
+ rtl?: boolean;
95
+ /** `InputRichMessage.skip_entity_detection` — see `RichDocument.noEntityDetection`. */
96
+ skipEntityDetection?: boolean;
97
+ }
98
+
99
+ /**
100
+ * assemble top-level blocks into a `RichDocument` in one call — the options-object
101
+ * counterpart of `html([...])`/`md([...])` + the fluent flag setters.
102
+ */
103
+ export function document(
104
+ blocks: readonly Insertable[] | Insertable,
105
+ options: DocumentOptions = {},
106
+ ): RichDocument {
107
+ const dialect = options.dialect ?? "html";
108
+ const entries: readonly Insertable[] = Array.isArray(blocks) ? blocks : [blocks];
109
+ const doc = new RichDocument(dialect, joinBlocks(entries, dialect));
110
+
111
+ if (options.rtl !== undefined) doc.rtl(options.rtl);
112
+ if (options.skipEntityDetection !== undefined) doc.noEntityDetection(options.skipEntityDetection);
113
+
114
+ return doc;
115
+ }
package/lib/message.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { InputRichMessage } from "@yaebal/types";
2
- import type { Insertable } from "./inline.js";
3
- export interface DocumentOptions {
4
- /** `InputRichMessage.is_rtl` — show the message right-to-left. */
5
- rtl?: boolean;
6
- /**
7
- * `InputRichMessage.skip_entity_detection` — turn off auto-detection of urls,
8
- * emails, `@mentions`, `#hashtags`, `$cashtags`, `/bot_commands`, and phone
9
- * numbers in plain text.
10
- */
11
- skipEntityDetection?: boolean;
12
- }
13
- /**
14
- * assemble top-level blocks (from blocks.ts) into an `InputRichMessage.html`
15
- * payload. pass the result straight to `sendRichMessage`/`sendRichMessageDraft`
16
- * or a `RichMessageDraft`.
17
- */
18
- export declare function document(blocks: Insertable[], options?: DocumentOptions): InputRichMessage;
19
- /**
20
- * a raw markdown payload — telegram parses `InputRichMessage.markdown` the same
21
- * way as `html`, but the extended block syntax (tables, `tg-thinking`, …) is not
22
- * documented in markdown form, so unlike `document()` this has no builder: pass a
23
- * literal string.
24
- */
25
- export declare function markdown(source: string, options?: DocumentOptions): InputRichMessage;
26
- //# sourceMappingURL=message.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,OAAO,GAAE,eAAoB,GAAG,gBAAgB,CAQ9F;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,gBAAgB,CAQxF"}
package/lib/message.js DELETED
@@ -1,31 +0,0 @@
1
- import { toHtml } from "./inline.js";
2
- /**
3
- * assemble top-level blocks (from blocks.ts) into an `InputRichMessage.html`
4
- * payload. pass the result straight to `sendRichMessage`/`sendRichMessageDraft`
5
- * or a `RichMessageDraft`.
6
- */
7
- export function document(blocks, options = {}) {
8
- return {
9
- html: blocks.map(toHtml).join(""),
10
- ...(options.rtl !== undefined ? { is_rtl: options.rtl } : {}),
11
- ...(options.skipEntityDetection !== undefined
12
- ? { skip_entity_detection: options.skipEntityDetection }
13
- : {}),
14
- };
15
- }
16
- /**
17
- * a raw markdown payload — telegram parses `InputRichMessage.markdown` the same
18
- * way as `html`, but the extended block syntax (tables, `tg-thinking`, …) is not
19
- * documented in markdown form, so unlike `document()` this has no builder: pass a
20
- * literal string.
21
- */
22
- export function markdown(source, options = {}) {
23
- return {
24
- markdown: source,
25
- ...(options.rtl !== undefined ? { is_rtl: options.rtl } : {}),
26
- ...(options.skipEntityDetection !== undefined
27
- ? { skip_entity_detection: options.skipEntityDetection }
28
- : {}),
29
- };
30
- }
31
- //# sourceMappingURL=message.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message.js","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAarC;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAoB,EAAE,UAA2B,EAAE;IAC3E,OAAO;QACN,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,OAAO,CAAC,mBAAmB,KAAK,SAAS;YAC5C,CAAC,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC,mBAAmB,EAAE;YACxD,CAAC,CAAC,EAAE,CAAC;KACN,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,UAA2B,EAAE;IACrE,OAAO;QACN,QAAQ,EAAE,MAAM;QAChB,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,OAAO,CAAC,mBAAmB,KAAK,SAAS;YAC5C,CAAC,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC,mBAAmB,EAAE;YACxD,CAAC,CAAC,EAAE,CAAC;KACN,CAAC;AACH,CAAC"}
package/src/message.ts DELETED
@@ -1,45 +0,0 @@
1
- import type { InputRichMessage } from "@yaebal/types";
2
- import type { Insertable } from "./inline.js";
3
- import { toHtml } from "./inline.js";
4
-
5
- export interface DocumentOptions {
6
- /** `InputRichMessage.is_rtl` — show the message right-to-left. */
7
- rtl?: boolean;
8
- /**
9
- * `InputRichMessage.skip_entity_detection` — turn off auto-detection of urls,
10
- * emails, `@mentions`, `#hashtags`, `$cashtags`, `/bot_commands`, and phone
11
- * numbers in plain text.
12
- */
13
- skipEntityDetection?: boolean;
14
- }
15
-
16
- /**
17
- * assemble top-level blocks (from blocks.ts) into an `InputRichMessage.html`
18
- * payload. pass the result straight to `sendRichMessage`/`sendRichMessageDraft`
19
- * or a `RichMessageDraft`.
20
- */
21
- export function document(blocks: Insertable[], options: DocumentOptions = {}): InputRichMessage {
22
- return {
23
- html: blocks.map(toHtml).join(""),
24
- ...(options.rtl !== undefined ? { is_rtl: options.rtl } : {}),
25
- ...(options.skipEntityDetection !== undefined
26
- ? { skip_entity_detection: options.skipEntityDetection }
27
- : {}),
28
- };
29
- }
30
-
31
- /**
32
- * a raw markdown payload — telegram parses `InputRichMessage.markdown` the same
33
- * way as `html`, but the extended block syntax (tables, `tg-thinking`, …) is not
34
- * documented in markdown form, so unlike `document()` this has no builder: pass a
35
- * literal string.
36
- */
37
- export function markdown(source: string, options: DocumentOptions = {}): InputRichMessage {
38
- return {
39
- markdown: source,
40
- ...(options.rtl !== undefined ? { is_rtl: options.rtl } : {}),
41
- ...(options.skipEntityDetection !== undefined
42
- ? { skip_entity_detection: options.skipEntityDetection }
43
- : {}),
44
- };
45
- }