@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.0

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 (295) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/adapters/clock.d.ts +3 -0
  5. package/dist/adapters/clock.js +4 -0
  6. package/dist/adapters/console-log.d.ts +13 -0
  7. package/dist/adapters/console-log.js +41 -0
  8. package/dist/adapters/d1-db.d.ts +5 -0
  9. package/dist/adapters/d1-db.js +37 -0
  10. package/dist/adapters/ids.d.ts +5 -0
  11. package/dist/adapters/ids.js +25 -0
  12. package/dist/adapters/index.d.ts +7 -0
  13. package/dist/adapters/keyring.d.ts +50 -0
  14. package/dist/adapters/keyring.js +174 -0
  15. package/dist/adapters/metrics.d.ts +28 -0
  16. package/dist/adapters/metrics.js +48 -0
  17. package/dist/adapters/r2-files.d.ts +20 -0
  18. package/dist/adapters/r2-files.js +53 -0
  19. package/dist/bin/cli.d.ts +1 -0
  20. package/dist/bin/cli.js +45 -0
  21. package/dist/client/broadcasts.d.ts +35 -0
  22. package/dist/client/commands.d.ts +23 -0
  23. package/dist/client/endpoint.d.ts +34 -0
  24. package/dist/client/events.d.ts +33 -0
  25. package/dist/client/index.d.ts +11 -0
  26. package/dist/client/index.js +3 -0
  27. package/dist/client/schedules.d.ts +42 -0
  28. package/dist/client/schema.d.ts +181 -0
  29. package/dist/client/send.d.ts +15 -0
  30. package/dist/client/templates.d.ts +47 -0
  31. package/dist/contracts/bots.d.ts +129 -0
  32. package/dist/contracts/bots.js +70 -0
  33. package/dist/contracts/broadcasts.d.ts +116 -0
  34. package/dist/contracts/broadcasts.js +63 -0
  35. package/dist/contracts/chats.d.ts +97 -0
  36. package/dist/contracts/chats.js +66 -0
  37. package/dist/contracts/commands.d.ts +52 -0
  38. package/dist/contracts/commands.js +53 -0
  39. package/dist/contracts/common.d.ts +38 -0
  40. package/dist/contracts/common.js +41 -0
  41. package/dist/contracts/deliveries.d.ts +103 -0
  42. package/dist/contracts/deliveries.js +74 -0
  43. package/dist/contracts/destinations.d.ts +24 -0
  44. package/dist/contracts/destinations.js +28 -0
  45. package/dist/contracts/events.d.ts +165 -0
  46. package/dist/contracts/events.js +98 -0
  47. package/dist/contracts/features.d.ts +20 -0
  48. package/dist/contracts/features.js +21 -0
  49. package/dist/contracts/health.d.ts +55 -0
  50. package/dist/contracts/health.js +58 -0
  51. package/dist/contracts/index.d.ts +22 -0
  52. package/dist/contracts/index.js +22 -0
  53. package/dist/contracts/labels.d.ts +38 -0
  54. package/dist/contracts/labels.js +52 -0
  55. package/dist/contracts/links.d.ts +51 -0
  56. package/dist/contracts/links.js +50 -0
  57. package/dist/contracts/meta.d.ts +132 -0
  58. package/dist/contracts/meta.js +85 -0
  59. package/dist/contracts/permissions.d.ts +24 -0
  60. package/dist/contracts/permissions.js +27 -0
  61. package/dist/contracts/problems.d.ts +106 -0
  62. package/dist/contracts/problems.js +101 -0
  63. package/dist/contracts/recipients.d.ts +36 -0
  64. package/dist/contracts/recipients.js +22 -0
  65. package/dist/contracts/schedules.d.ts +276 -0
  66. package/dist/contracts/schedules.js +143 -0
  67. package/dist/contracts/send.d.ts +126 -0
  68. package/dist/contracts/send.js +69 -0
  69. package/dist/contracts/subscriptions.d.ts +38 -0
  70. package/dist/contracts/subscriptions.js +31 -0
  71. package/dist/contracts/templates.d.ts +358 -0
  72. package/dist/contracts/templates.js +174 -0
  73. package/dist/contracts/users.d.ts +15 -0
  74. package/dist/contracts/users.js +19 -0
  75. package/dist/core/guards.d.ts +22 -0
  76. package/dist/core/guards.js +47 -0
  77. package/dist/core/html/escape.d.ts +2 -0
  78. package/dist/core/html/escape.js +36 -0
  79. package/dist/core/html/tokenize.js +65 -0
  80. package/dist/core/index.d.ts +18 -0
  81. package/dist/core/pacing.d.ts +1 -0
  82. package/dist/core/pacing.js +86 -0
  83. package/dist/core/random.d.ts +2 -0
  84. package/dist/core/random.js +38 -0
  85. package/dist/core/recipients.d.ts +1 -0
  86. package/dist/core/recipients.js +86 -0
  87. package/dist/core/schedule/describe.d.ts +3 -0
  88. package/dist/core/schedule/describe.js +74 -0
  89. package/dist/core/schedule/index.d.ts +4 -0
  90. package/dist/core/schedule/policy.d.ts +1 -0
  91. package/dist/core/schedule/policy.js +37 -0
  92. package/dist/core/schedule/rule.d.ts +25 -0
  93. package/dist/core/schedule/rule.js +142 -0
  94. package/dist/core/schedule/tz.d.ts +2 -0
  95. package/dist/core/schedule/tz.js +59 -0
  96. package/dist/core/split/plain.d.ts +2 -0
  97. package/dist/core/split/plain.js +23 -0
  98. package/dist/core/split/split.d.ts +15 -0
  99. package/dist/core/split/split.js +158 -0
  100. package/dist/core/template/ast.d.ts +58 -0
  101. package/dist/core/template/ast.js +5 -0
  102. package/dist/core/template/expr.js +108 -0
  103. package/dist/core/template/filters.d.ts +20 -0
  104. package/dist/core/template/filters.js +127 -0
  105. package/dist/core/template/format.d.ts +17 -0
  106. package/dist/core/template/format.js +88 -0
  107. package/dist/core/template/index.d.ts +20 -0
  108. package/dist/core/template/index.js +29 -0
  109. package/dist/core/template/parse.d.ts +8 -0
  110. package/dist/core/template/parse.js +369 -0
  111. package/dist/core/template/render.d.ts +17 -0
  112. package/dist/core/template/render.js +118 -0
  113. package/dist/core/template/shape.d.ts +28 -0
  114. package/dist/core/template/shape.js +151 -0
  115. package/dist/core/template/tags.js +105 -0
  116. package/dist/core/template/validate.d.ts +10 -0
  117. package/dist/core/template/validate.js +144 -0
  118. package/dist/core/template/variables.d.ts +15 -0
  119. package/dist/core/template/variables.js +41 -0
  120. package/dist/do/index.d.ts +3 -0
  121. package/dist/do/index.js +3 -0
  122. package/dist/do/telegram-bot-do.d.ts +45 -0
  123. package/dist/do/telegram-bot-do.js +219 -0
  124. package/dist/engine/actions/callback.js +256 -0
  125. package/dist/engine/actions/tokens.d.ts +2 -0
  126. package/dist/engine/actions/tokens.js +51 -0
  127. package/dist/engine/bot-api.js +20 -0
  128. package/dist/engine/bot-features.js +39 -0
  129. package/dist/engine/chat-state.js +66 -0
  130. package/dist/engine/content/audience.js +54 -0
  131. package/dist/engine/content/buttons.d.ts +1 -0
  132. package/dist/engine/content/buttons.js +113 -0
  133. package/dist/engine/content/fanout.js +284 -0
  134. package/dist/engine/content/intersect.d.ts +6 -0
  135. package/dist/engine/content/intersect.js +26 -0
  136. package/dist/engine/content/providers.d.ts +6 -0
  137. package/dist/engine/content/providers.js +142 -0
  138. package/dist/engine/content/recipients.js +128 -0
  139. package/dist/engine/content/render.js +138 -0
  140. package/dist/engine/content/reply.js +156 -0
  141. package/dist/engine/content/report-files.js +78 -0
  142. package/dist/engine/content/template-rows.js +74 -0
  143. package/dist/engine/dialogs/dialog.d.ts +2 -0
  144. package/dist/engine/dialogs/dialog.js +375 -0
  145. package/dist/engine/dispatch/builtins.js +198 -0
  146. package/dist/engine/dispatch/commands.js +194 -0
  147. package/dist/engine/dispatch/context.d.ts +2 -0
  148. package/dist/engine/dispatch/context.js +194 -0
  149. package/dist/engine/dispatch/dispatcher.js +66 -0
  150. package/dist/engine/dispatch/handlers.d.ts +1 -0
  151. package/dist/engine/dispatch/handlers.js +103 -0
  152. package/dist/engine/dispatch/lifecycle.js +85 -0
  153. package/dist/engine/dispatch/link-chat.js +31 -0
  154. package/dist/engine/dispatch/linking.js +273 -0
  155. package/dist/engine/errors.d.ts +60 -0
  156. package/dist/engine/errors.js +72 -0
  157. package/dist/engine/events/filters.d.ts +5 -0
  158. package/dist/engine/events/filters.js +82 -0
  159. package/dist/engine/events/kick.js +30 -0
  160. package/dist/engine/events/payload.d.ts +3 -0
  161. package/dist/engine/events/payload.js +16 -0
  162. package/dist/engine/events/tick.js +208 -0
  163. package/dist/engine/i18n.d.ts +12 -0
  164. package/dist/engine/i18n.js +55 -0
  165. package/dist/engine/inbound/files.js +121 -0
  166. package/dist/engine/inbound/handler.js +46 -0
  167. package/dist/engine/jobs.d.ts +32 -0
  168. package/dist/engine/jobs.js +85 -0
  169. package/dist/engine/link-codes.js +46 -0
  170. package/dist/engine/metrics.js +12 -0
  171. package/dist/engine/options.d.ts +369 -0
  172. package/dist/engine/options.js +81 -0
  173. package/dist/engine/outbox/dead-letter.js +78 -0
  174. package/dist/engine/outbox/do-registry.d.ts +10 -0
  175. package/dist/engine/outbox/do-registry.js +10 -0
  176. package/dist/engine/outbox/do-transport.d.ts +33 -0
  177. package/dist/engine/outbox/do-transport.js +73 -0
  178. package/dist/engine/outbox/drain.d.ts +13 -0
  179. package/dist/engine/outbox/drain.js +381 -0
  180. package/dist/engine/outbox/enqueue.d.ts +1 -0
  181. package/dist/engine/outbox/enqueue.js +63 -0
  182. package/dist/engine/outbox/select.js +24 -0
  183. package/dist/engine/outbox/send.js +362 -0
  184. package/dist/engine/outbox/transport.d.ts +35 -0
  185. package/dist/engine/outbox/transport.js +175 -0
  186. package/dist/engine/outbox/wake.js +28 -0
  187. package/dist/engine/registry.d.ts +502 -0
  188. package/dist/engine/registry.js +258 -0
  189. package/dist/engine/rows.d.ts +25 -0
  190. package/dist/engine/rows.js +131 -0
  191. package/dist/engine/runtime.d.ts +95 -0
  192. package/dist/engine/runtime.js +149 -0
  193. package/dist/engine/schedules/fire.js +208 -0
  194. package/dist/engine/schedules/resume.js +75 -0
  195. package/dist/engine/schedules/rows.js +69 -0
  196. package/dist/engine/schedules/tick.js +30 -0
  197. package/dist/engine/schedules/wake.js +50 -0
  198. package/dist/engine/state.d.ts +11 -0
  199. package/dist/engine/state.js +78 -0
  200. package/dist/locales/en.js +339 -0
  201. package/dist/locales/en.json +341 -0
  202. package/dist/locales/mr.js +339 -0
  203. package/dist/locales/mr.json +341 -0
  204. package/dist/ports/db.d.ts +22 -0
  205. package/dist/ports/index.d.ts +4 -0
  206. package/dist/ports/ports.d.ts +257 -0
  207. package/dist/ports/sql.d.ts +39 -0
  208. package/dist/ports/sql.js +76 -0
  209. package/dist/renderers/csv.d.ts +14 -0
  210. package/dist/renderers/csv.js +51 -0
  211. package/dist/renderers/html.d.ts +44 -0
  212. package/dist/renderers/html.js +81 -0
  213. package/dist/renderers/index.d.ts +5 -0
  214. package/dist/renderers/index.js +4 -0
  215. package/dist/renderers/table.d.ts +30 -0
  216. package/dist/renderers/table.js +114 -0
  217. package/dist/routes/admin.js +146 -0
  218. package/dist/routes/app.d.ts +3111 -0
  219. package/dist/routes/app.js +9 -0
  220. package/dist/routes/broadcasts.js +37 -0
  221. package/dist/routes/commands.js +35 -0
  222. package/dist/routes/content.js +12 -0
  223. package/dist/routes/events.js +36 -0
  224. package/dist/routes/http.d.ts +30 -0
  225. package/dist/routes/http.js +223 -0
  226. package/dist/routes/me.js +59 -0
  227. package/dist/routes/schedules.js +62 -0
  228. package/dist/routes/send.js +24 -0
  229. package/dist/routes/templates.js +70 -0
  230. package/dist/routes/webhook.js +147 -0
  231. package/dist/schema/columns.js +307 -0
  232. package/dist/schema/ddl.d.ts +7 -0
  233. package/dist/schema/ddl.js +45 -0
  234. package/dist/schema/index.d.ts +4255 -0
  235. package/dist/schema/index.js +44 -0
  236. package/dist/schema/reference.sql +359 -0
  237. package/dist/schema/tables.d.ts +4243 -0
  238. package/dist/schema/tables.js +327 -0
  239. package/dist/schema/version.d.ts +5 -0
  240. package/dist/schema/version.js +7 -0
  241. package/dist/server/index.d.ts +55 -0
  242. package/dist/server/index.js +46 -0
  243. package/dist/server/plugin.d.ts +81 -0
  244. package/dist/server/plugin.js +247 -0
  245. package/dist/server/version.d.ts +2 -0
  246. package/dist/server/version.js +4 -0
  247. package/dist/services/ai-tool.d.ts +26 -0
  248. package/dist/services/ai-tool.js +148 -0
  249. package/dist/services/bots.js +300 -0
  250. package/dist/services/broadcasts.js +390 -0
  251. package/dist/services/chats.js +83 -0
  252. package/dist/services/cleanup.js +83 -0
  253. package/dist/services/commands.js +140 -0
  254. package/dist/services/deliveries.js +53 -0
  255. package/dist/services/destinations.js +91 -0
  256. package/dist/services/event-rules.js +229 -0
  257. package/dist/services/events.js +20 -0
  258. package/dist/services/idempotency.js +64 -0
  259. package/dist/services/links.js +200 -0
  260. package/dist/services/menu.js +95 -0
  261. package/dist/services/ops.js +90 -0
  262. package/dist/services/rekey.js +63 -0
  263. package/dist/services/reports.js +87 -0
  264. package/dist/services/schedules.js +214 -0
  265. package/dist/services/send.d.ts +14 -0
  266. package/dist/services/send.js +421 -0
  267. package/dist/services/subscriptions.js +28 -0
  268. package/dist/services/templates.js +259 -0
  269. package/dist/telegram/classify.d.ts +30 -0
  270. package/dist/telegram/classify.js +164 -0
  271. package/dist/telegram/client.d.ts +73 -0
  272. package/dist/telegram/client.js +135 -0
  273. package/dist/telegram/errors.d.ts +37 -0
  274. package/dist/telegram/errors.js +62 -0
  275. package/dist/telegram/inbound.d.ts +40 -0
  276. package/dist/telegram/inbound.js +72 -0
  277. package/dist/telegram/index.d.ts +8 -0
  278. package/dist/telegram/multipart.d.ts +5 -0
  279. package/dist/telegram/multipart.js +47 -0
  280. package/dist/telegram/types.d.ts +271 -0
  281. package/dist/telegram/update.d.ts +833 -0
  282. package/dist/telegram/update.js +129 -0
  283. package/dist/testing/fake-html.d.ts +9 -0
  284. package/dist/testing/fake-html.js +83 -0
  285. package/dist/testing/fake-telegram.d.ts +206 -0
  286. package/dist/testing/fake-telegram.js +856 -0
  287. package/dist/testing/index.d.ts +7 -0
  288. package/dist/testing/index.js +7 -0
  289. package/dist/testing/reference-sql.d.ts +4 -0
  290. package/dist/testing/reference-sql.js +8 -0
  291. package/dist/testing/test-plugin.d.ts +126 -0
  292. package/dist/testing/test-plugin.js +182 -0
  293. package/dist/testing/updates.d.ts +100 -0
  294. package/dist/testing/updates.js +212 -0
  295. package/package.json +113 -3
@@ -0,0 +1,369 @@
1
+ import { ENTITY_RE } from "../html/escape.js";
2
+ import { pathText } from "./ast.js";
3
+ import { parseExpr, parsePath } from "./expr.js";
4
+ import { ALLOWED_TAGS, checkAttrs, isAllowedTag } from "./tags.js";
5
+ // src/core/template/parse.ts
6
+ const OPEN_RE = /<([A-Za-z][A-Za-z0-9-]*)/y;
7
+ const CLOSE_RE = /<\/([A-Za-z][A-Za-z0-9-]*)\s*>/y;
8
+ const ATTR_NAME_RE = /[A-Za-z][A-Za-z0-9-]*/y;
9
+ /** Parses a template (`{{…}}` tags inside the allowed Telegram HTML) into an AST. Never throws; errors carry 1-based positions. */
10
+ function parseTemplate(src) {
11
+ const lineStarts = [0];
12
+ for (let k = 0; k < src.length; k++) if (src[k] === "\n") lineStarts.push(k + 1);
13
+ const pos = (at) => {
14
+ let lo = 0;
15
+ let hi = lineStarts.length - 1;
16
+ while (lo < hi) {
17
+ const mid = lo + hi + 1 >> 1;
18
+ if ((lineStarts[mid] ?? 0) <= at) lo = mid;
19
+ else hi = mid - 1;
20
+ }
21
+ return {
22
+ line: lo + 1,
23
+ column: at - (lineStarts[lo] ?? 0) + 1
24
+ };
25
+ };
26
+ const errors = [];
27
+ const fail = (code, at, message) => errors.push({
28
+ code,
29
+ message,
30
+ ...pos(at)
31
+ });
32
+ const ast = {
33
+ type: "root",
34
+ children: []
35
+ };
36
+ const stack = [ast];
37
+ const top = () => stack[stack.length - 1] ?? ast;
38
+ const rejected = /* @__PURE__ */ new Map();
39
+ let buf = "";
40
+ let bufStart = 0;
41
+ const append = (s, at) => {
42
+ if (!buf) bufStart = at;
43
+ buf += s;
44
+ };
45
+ const flush = () => {
46
+ if (buf) top().children.push({
47
+ type: "text",
48
+ value: buf,
49
+ ...pos(bufStart)
50
+ });
51
+ buf = "";
52
+ };
53
+ const push = (node) => {
54
+ flush();
55
+ top().children.push(node);
56
+ };
57
+ /** A section/comment tag alone on its line removes the whole line (as in Mustache). Returns the resume index. */
58
+ const standalone = (start, end) => {
59
+ let ls = start;
60
+ while (ls > 0 && (src[ls - 1] === " " || src[ls - 1] === " ")) ls--;
61
+ if (ls > 0 && src[ls - 1] !== "\n") return null;
62
+ let le = end;
63
+ while (le < src.length && (src[le] === " " || src[le] === " ")) le++;
64
+ if (le < src.length && src[le] !== "\n" && !(src[le] === "\r" && src[le + 1] === "\n")) return null;
65
+ buf = buf.replace(/[ \t]*$/, "");
66
+ return le >= src.length ? src.length : le + (src[le] === "\r" ? 2 : 1);
67
+ };
68
+ const varNode = (inner, innerStart, at) => {
69
+ const e = parseExpr(inner);
70
+ if ("message" in e) {
71
+ fail("syntax", innerStart + e.offset, e.message);
72
+ return null;
73
+ }
74
+ return {
75
+ type: "var",
76
+ path: e.path,
77
+ filters: e.filters.map((f, k) => ({
78
+ ...f,
79
+ ...pos(innerStart + (e.filterOffsets[k] ?? 0))
80
+ })),
81
+ ...pos(at)
82
+ };
83
+ };
84
+ const mustache = (i) => {
85
+ if (src[i + 2] === "{") {
86
+ fail("raw_not_allowed", i, "{{{raw}}} output is not allowed; every value is escaped");
87
+ const end = src.indexOf("}}}", i + 3);
88
+ return end < 0 ? src.length : end + 3;
89
+ }
90
+ const end = src.indexOf("}}", i + 2);
91
+ if (end < 0) {
92
+ fail("syntax", i, "unclosed {{");
93
+ return src.length;
94
+ }
95
+ const raw = src.slice(i + 2, end);
96
+ const lead = raw.length - raw.trimStart().length;
97
+ const sigil = raw.trimStart()[0] ?? "";
98
+ const body = raw.trimStart().slice(1);
99
+ const tagEnd = end + 2;
100
+ switch (sigil) {
101
+ case "!": return standalone(i, tagEnd) ?? tagEnd;
102
+ case "#":
103
+ case "^": {
104
+ const path = parsePath(body);
105
+ if (path === null || path.length === 0) {
106
+ fail("syntax", i + 2 + lead + 1, `expected a variable name after {{${sigil}`);
107
+ return tagEnd;
108
+ }
109
+ const next = standalone(i, tagEnd);
110
+ const node = {
111
+ type: sigil === "#" ? "section" : "inverted",
112
+ path,
113
+ children: [],
114
+ ...pos(i)
115
+ };
116
+ push(node);
117
+ stack.push(node);
118
+ return next ?? tagEnd;
119
+ }
120
+ case "/": {
121
+ const path = parsePath(body);
122
+ if (path === null) {
123
+ fail("syntax", i + 2 + lead + 1, "expected a variable name after {{/");
124
+ return tagEnd;
125
+ }
126
+ const next = standalone(i, tagEnd);
127
+ flush();
128
+ closeSection(pathText(path), i);
129
+ return next ?? tagEnd;
130
+ }
131
+ case "&":
132
+ fail("raw_not_allowed", i, "{{&raw}} output is not allowed; every value is escaped");
133
+ return tagEnd;
134
+ case ">":
135
+ fail("syntax", i, "partials ({{>name}}) are not supported");
136
+ return tagEnd;
137
+ case "=":
138
+ fail("syntax", i, "changing delimiters is not supported");
139
+ return tagEnd;
140
+ default: {
141
+ const node = varNode(raw, i + 2, i);
142
+ if (node) push(node);
143
+ return tagEnd;
144
+ }
145
+ }
146
+ };
147
+ const closeSection = (name, at) => {
148
+ let k = stack.length - 1;
149
+ while (k > 0) {
150
+ const c = stack[k];
151
+ if (c && (c.type === "section" || c.type === "inverted") && pathText(c.path) === name) break;
152
+ k--;
153
+ }
154
+ if (k === 0) {
155
+ fail("syntax", at, `{{/${name}}} has no matching {{#${name}}}`);
156
+ return;
157
+ }
158
+ while (stack.length - 1 > k) {
159
+ const c = stack.pop();
160
+ if (c?.type === "element") fail("nesting_invalid", at, `<${c.name}> must be closed before {{/${name}}}`);
161
+ else if (c && c.type !== "root") fail("unclosed", at, `{{#${pathText(c.path)}}} must be closed before {{/${name}}}`);
162
+ }
163
+ stack.pop();
164
+ };
165
+ const closeTag = (name, at) => {
166
+ let k = stack.length - 1;
167
+ while (k > 0) {
168
+ const c = stack[k];
169
+ if (c?.type !== "element") break;
170
+ if (c.name === name) break;
171
+ k--;
172
+ }
173
+ const c = stack[k];
174
+ if (k === 0 || !c || c.type !== "element" || c.name !== name) {
175
+ const inSection = stack.some((s) => s.type === "element" && s.name === name);
176
+ fail("nesting_invalid", at, inSection ? `</${name}> must close inside the same {{#section}} it opened in` : `</${name}> has no opening tag`);
177
+ return;
178
+ }
179
+ while (stack.length - 1 > k) {
180
+ const inner = stack.pop();
181
+ if (inner?.type === "element") fail("unclosed", at, `<${inner.name}> is not closed before </${name}>`);
182
+ }
183
+ stack.pop();
184
+ };
185
+ const nestingError = (name) => {
186
+ const parent = top();
187
+ for (const c of stack) {
188
+ if (c.type !== "element") continue;
189
+ if (c.name === "code") return "no tags are allowed inside <code>";
190
+ if (name === "blockquote" && c.name === "blockquote") return "<blockquote> cannot be nested";
191
+ if (name === "a" && c.name === "a") return "<a> cannot be nested";
192
+ }
193
+ if (parent.type === "element" && parent.name === "pre" && name !== "code") return "<pre> may contain only <code>";
194
+ return null;
195
+ };
196
+ /** Parses `<tag …>` at i. Returns the resume index. */
197
+ const openTag = (i) => {
198
+ OPEN_RE.lastIndex = i;
199
+ const rawName = OPEN_RE.exec(src)?.[1] ?? "";
200
+ let j = i + 1 + rawName.length;
201
+ const attrs = [];
202
+ for (;;) {
203
+ while (j < src.length && /\s/.test(src[j] ?? "")) j++;
204
+ if (j >= src.length) {
205
+ fail("syntax", i, `unclosed <${rawName}`);
206
+ return src.length;
207
+ }
208
+ if (src[j] === ">") break;
209
+ if (src[j] === "/" && src[j + 1] === ">") {
210
+ fail("syntax", i, "self-closing tags are not supported");
211
+ j++;
212
+ break;
213
+ }
214
+ ATTR_NAME_RE.lastIndex = j;
215
+ const an = ATTR_NAME_RE.exec(src)?.[0];
216
+ if (!an) {
217
+ fail("syntax", j, `unexpected "${src[j]}" in <${rawName}>`);
218
+ const close = src.indexOf(">", j);
219
+ return close < 0 ? src.length : close + 1;
220
+ }
221
+ j += an.length;
222
+ while (j < src.length && /\s/.test(src[j] ?? "")) j++;
223
+ if (src[j] !== "=") {
224
+ attrs.push({
225
+ name: an.toLowerCase(),
226
+ value: null
227
+ });
228
+ continue;
229
+ }
230
+ j++;
231
+ while (j < src.length && /\s/.test(src[j] ?? "")) j++;
232
+ const q = src[j];
233
+ if (q !== "\"" && q !== "'") {
234
+ fail("attr_invalid", j, "attribute values must be quoted");
235
+ const close = src.indexOf(">", j);
236
+ return close < 0 ? src.length : close + 1;
237
+ }
238
+ j++;
239
+ const parts = [];
240
+ let text = "";
241
+ while (j < src.length && src[j] !== q) {
242
+ if (src.startsWith("{{", j)) {
243
+ const end = src.indexOf("}}", j + 2);
244
+ if (end < 0) {
245
+ fail("syntax", j, "unclosed {{");
246
+ return src.length;
247
+ }
248
+ const inner = src.slice(j + 2, end);
249
+ const s = inner.trimStart()[0] ?? "";
250
+ if (src[j + 2] === "{" || "#^/!&>=".includes(s)) fail(src[j + 2] === "{" ? "raw_not_allowed" : "syntax", j, "only {{variables}} are allowed in attributes");
251
+ else {
252
+ const node = varNode(inner, j + 2, j);
253
+ if (node) {
254
+ if (text) parts.push({
255
+ type: "text",
256
+ value: text
257
+ });
258
+ text = "";
259
+ parts.push(node);
260
+ }
261
+ }
262
+ j = end + (src[end + 2] === "}" ? 3 : 2);
263
+ continue;
264
+ }
265
+ const c = src[j] ?? "";
266
+ if (c === "&") {
267
+ ENTITY_RE.lastIndex = j;
268
+ const e = ENTITY_RE.exec(src);
269
+ if (e) {
270
+ text += e[0];
271
+ j += e[0].length;
272
+ continue;
273
+ }
274
+ text += "&amp;";
275
+ } else if (c === "<") text += "&lt;";
276
+ else if (c === ">") text += "&gt;";
277
+ else text += c;
278
+ j++;
279
+ }
280
+ if (j >= src.length) {
281
+ fail("syntax", i, `unclosed attribute in <${rawName}>`);
282
+ return src.length;
283
+ }
284
+ if (text) parts.push({
285
+ type: "text",
286
+ value: text
287
+ });
288
+ attrs.push({
289
+ name: an.toLowerCase(),
290
+ value: parts
291
+ });
292
+ j++;
293
+ }
294
+ const end = j + 1;
295
+ const name = rawName.toLowerCase();
296
+ if (rawName !== name) {
297
+ fail("tag_not_allowed", i, `write tags in lowercase: <${name}>`);
298
+ rejected.set(rawName, (rejected.get(rawName) ?? 0) + 1);
299
+ return end;
300
+ }
301
+ if (!isAllowedTag(name)) {
302
+ fail("tag_not_allowed", i, `<${name}> is not allowed; Telegram supports ${ALLOWED_TAGS.join(", ")}`);
303
+ rejected.set(name, (rejected.get(name) ?? 0) + 1);
304
+ return end;
305
+ }
306
+ for (const msg of checkAttrs(name, attrs)) fail(msg.includes("href") ? "url_invalid" : "attr_invalid", i, msg);
307
+ const nest = nestingError(name);
308
+ if (nest) fail("nesting_invalid", i, nest);
309
+ const node = {
310
+ type: "element",
311
+ name,
312
+ attrs,
313
+ children: [],
314
+ ...pos(i)
315
+ };
316
+ push(node);
317
+ stack.push(node);
318
+ return end;
319
+ };
320
+ let i = 0;
321
+ while (i < src.length) {
322
+ const c = src[i] ?? "";
323
+ if (c === "{" && src[i + 1] === "{") i = mustache(i);
324
+ else if (c === "<" && /[A-Za-z]/.test(src[i + 1] ?? "")) i = openTag(i);
325
+ else if (c === "<" && src[i + 1] === "/") {
326
+ CLOSE_RE.lastIndex = i;
327
+ const m = CLOSE_RE.exec(src);
328
+ if (!m) {
329
+ fail("syntax", i, "invalid closing tag");
330
+ append("&lt;", i);
331
+ i++;
332
+ continue;
333
+ }
334
+ const raw = m[1] ?? "";
335
+ const left = rejected.get(raw) ?? 0;
336
+ if (left > 0) rejected.set(raw, left - 1);
337
+ else if (!isAllowedTag(raw)) fail("tag_not_allowed", i, `</${raw}> is not allowed`);
338
+ else {
339
+ flush();
340
+ closeTag(raw, i);
341
+ }
342
+ i += m[0].length;
343
+ } else if (c === "&") {
344
+ ENTITY_RE.lastIndex = i;
345
+ const m = ENTITY_RE.exec(src);
346
+ append(m ? m[0] : "&amp;", i);
347
+ i += m ? m[0].length : 1;
348
+ } else {
349
+ append(c === "<" ? "&lt;" : c === ">" ? "&gt;" : c, i);
350
+ i++;
351
+ }
352
+ }
353
+ flush();
354
+ for (let k = stack.length - 1; k > 0; k--) {
355
+ const c = stack[k];
356
+ if (!c || c.type === "root") continue;
357
+ errors.push({
358
+ code: "unclosed",
359
+ message: c.type === "element" ? `<${c.name}> is not closed` : `{{#${pathText(c.path)}}} is not closed`,
360
+ line: c.line,
361
+ column: c.column
362
+ });
363
+ }
364
+ return {
365
+ ast,
366
+ errors
367
+ };
368
+ }
369
+ export { parseTemplate };
@@ -0,0 +1,17 @@
1
+ import { CoreTemplateError, TemplateAst } from "./ast.js";
2
+ import { FilterContext, FilterRegistry } from "./filters.js";
3
+ /** Data and formatting context for {@link renderTemplate}. */
4
+ export interface RenderContext extends FilterContext {
5
+ /** Root variables object. */
6
+ vars: unknown;
7
+ /** Host filters (override built-ins). */
8
+ filters?: FilterRegistry | undefined;
9
+ }
10
+ /** Result of {@link renderTemplate}. */
11
+ export interface RenderResult {
12
+ html: string;
13
+ /** Filter failures and dropped links (rendering never throws on data). */
14
+ warnings: CoreTemplateError[];
15
+ }
16
+ /** Renders an AST to Telegram HTML. Values are escaped; never throws on data (problems become `warnings`). */
17
+ export declare function renderTemplate(ast: TemplateAst, ctx: RenderContext): RenderResult;
@@ -0,0 +1,118 @@
1
+ import { escapeHtml } from "../html/escape.js";
2
+ import { renderedAttrOk } from "./tags.js";
3
+ import { resolveFilter } from "./filters.js";
4
+ // src/core/template/render.ts
5
+ /** Own-property lookup only, so `{{constructor}}` or `{{__proto__}}` never reach prototypes. */
6
+ function get(obj, key) {
7
+ if (obj === null || typeof obj !== "object") return void 0;
8
+ if (Array.isArray(obj) && key === "length") return obj.length;
9
+ return Object.hasOwn(obj, key) ? obj[key] : void 0;
10
+ }
11
+ function lookup(path, stack) {
12
+ if (path.length === 0) return stack[stack.length - 1];
13
+ const [head, ...rest] = path;
14
+ if (head === void 0) return void 0;
15
+ let value;
16
+ for (let k = stack.length - 1; k >= 0; k--) {
17
+ const scope = stack[k];
18
+ if (scope !== null && typeof scope === "object" && Object.hasOwn(scope, head)) {
19
+ value = get(scope, head);
20
+ break;
21
+ }
22
+ }
23
+ for (const key of rest) value = get(value, key);
24
+ return value;
25
+ }
26
+ function isFalsy(v) {
27
+ return v === void 0 || v === null || v === false || v === "" || v === 0 || Number.isNaN(v) || Array.isArray(v) && v.length === 0;
28
+ }
29
+ function stringify(v) {
30
+ if (v === null || v === void 0) return "";
31
+ if (typeof v === "string") return v;
32
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "";
33
+ if (typeof v === "boolean" || typeof v === "bigint") return String(v);
34
+ if (Array.isArray(v)) return v.map(stringify).filter(Boolean).join(", ");
35
+ return "";
36
+ }
37
+ /** Renders an AST to Telegram HTML. Values are escaped; never throws on data (problems become `warnings`). */
38
+ function renderTemplate(ast, ctx) {
39
+ const warnings = [];
40
+ const value = (node, stack) => {
41
+ let v = lookup(node.path, stack);
42
+ for (const call of node.filters) {
43
+ const f = resolveFilter(call.name, ctx.filters);
44
+ if (!f) {
45
+ warnings.push({
46
+ code: "filter_unknown",
47
+ message: `unknown filter "${call.name}"`,
48
+ line: call.line,
49
+ column: call.column
50
+ });
51
+ continue;
52
+ }
53
+ try {
54
+ v = f.apply(v, call.args, ctx);
55
+ } catch (e) {
56
+ const msg = e instanceof Error ? e.message : "failed";
57
+ warnings.push({
58
+ code: "render",
59
+ message: `filter "${call.name}": ${msg}`,
60
+ line: call.line,
61
+ column: call.column
62
+ });
63
+ v = "";
64
+ }
65
+ }
66
+ return escapeHtml(stringify(v));
67
+ };
68
+ const element = (node, stack) => {
69
+ let attrs = "";
70
+ for (const a of node.attrs) {
71
+ if (a.value === null) {
72
+ attrs += ` ${a.name}`;
73
+ continue;
74
+ }
75
+ const text = a.value.map((p) => p.type === "text" ? p.value : value(p, stack)).join("");
76
+ if (!renderedAttrOk(node.name, a.name, text)) {
77
+ warnings.push({
78
+ code: "url_invalid",
79
+ message: `<${node.name} ${a.name}> rendered an unsafe value; the tag was dropped`,
80
+ line: node.line,
81
+ column: node.column
82
+ });
83
+ return nodes(node.children, stack);
84
+ }
85
+ attrs += ` ${a.name}="${text}"`;
86
+ }
87
+ return `<${node.name}${attrs}>${nodes(node.children, stack)}</${node.name}>`;
88
+ };
89
+ const nodes = (list, stack) => {
90
+ let out = "";
91
+ for (const node of list) switch (node.type) {
92
+ case "text":
93
+ out += node.value;
94
+ break;
95
+ case "var":
96
+ out += value(node, stack);
97
+ break;
98
+ case "element":
99
+ out += element(node, stack);
100
+ break;
101
+ case "section": {
102
+ const v = lookup(node.path, stack);
103
+ if (isFalsy(v)) break;
104
+ if (Array.isArray(v)) for (const item of v) out += nodes(node.children, [...stack, item]);
105
+ else if (v === true) out += nodes(node.children, stack);
106
+ else out += nodes(node.children, [...stack, v]);
107
+ break;
108
+ }
109
+ case "inverted": if (isFalsy(lookup(node.path, stack))) out += nodes(node.children, stack);
110
+ }
111
+ return out;
112
+ };
113
+ return {
114
+ html: nodes(ast.children, [ctx.vars]),
115
+ warnings
116
+ };
117
+ }
118
+ export { isFalsy, renderTemplate };
@@ -0,0 +1,28 @@
1
+ /** Simplified type of template variables (from {@link zodToShape}), used to check paths and sections. */
2
+ export type Shape = {
3
+ kind: "object";
4
+ fields: Readonly<Record<string, Shape>>;
5
+ optional: boolean;
6
+ } | {
7
+ kind: "array";
8
+ item: Shape;
9
+ optional: boolean;
10
+ } | {
11
+ kind: "enum";
12
+ values: readonly string[];
13
+ optional: boolean;
14
+ } | {
15
+ kind: "string" | "number" | "boolean" | "date" | "record" | "unknown";
16
+ optional: boolean;
17
+ };
18
+ /** Zod 4 schema (objects, arrays, optional, nullable, default, primitives, enums, literals, unions) → `Shape`. */
19
+ export declare function zodToShape(schema: unknown, depth?: number): Shape;
20
+ /** One variable path offered by a {@link Shape} (see {@link shapeVariables}). */
21
+ export interface ShapeVariable {
22
+ /** Dotted path; list items use `[]` (`projects[].name`). */
23
+ path: string;
24
+ kind: Shape["kind"];
25
+ optional: boolean;
26
+ }
27
+ /** Every variable a schema offers, for UI variable pickers. */
28
+ export declare function shapeVariables(shape: Shape, prefix?: string, out?: ShapeVariable[]): ShapeVariable[];
@@ -0,0 +1,151 @@
1
+ // src/core/template/shape.ts
2
+ function isZodLike(v) {
3
+ if (v === null || typeof v !== "object" || !("_zod" in v)) return false;
4
+ const z = v._zod;
5
+ return z !== null && typeof z === "object" && "def" in z;
6
+ }
7
+ const OPTIONAL_WRAPPERS = /* @__PURE__ */ new Set([
8
+ "optional",
9
+ "nullable",
10
+ "nullish"
11
+ ]);
12
+ const TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
13
+ "default",
14
+ "prefault",
15
+ "readonly",
16
+ "catch",
17
+ "nonoptional",
18
+ "success"
19
+ ]);
20
+ function withOptional(s, optional) {
21
+ return optional && !s.optional ? {
22
+ ...s,
23
+ optional: true
24
+ } : s;
25
+ }
26
+ /** Zod 4 schema (objects, arrays, optional, nullable, default, primitives, enums, literals, unions) → `Shape`. */
27
+ function zodToShape(schema, depth = 0) {
28
+ if (!isZodLike(schema) || depth > 32) return {
29
+ kind: "unknown",
30
+ optional: false
31
+ };
32
+ const def = schema._zod.def;
33
+ const t = def.type;
34
+ if (OPTIONAL_WRAPPERS.has(t) && def.innerType) return withOptional(zodToShape(def.innerType, depth + 1), true);
35
+ if (TRANSPARENT_WRAPPERS.has(t) && def.innerType) {
36
+ const inner = zodToShape(def.innerType, depth + 1);
37
+ return t === "nonoptional" ? {
38
+ ...inner,
39
+ optional: false
40
+ } : inner;
41
+ }
42
+ switch (t) {
43
+ case "object": {
44
+ const fields = {};
45
+ for (const [k, v] of Object.entries(def.shape ?? {})) fields[k] = zodToShape(v, depth + 1);
46
+ return {
47
+ kind: "object",
48
+ fields,
49
+ optional: false
50
+ };
51
+ }
52
+ case "array": return {
53
+ kind: "array",
54
+ item: zodToShape(def.element, depth + 1),
55
+ optional: false
56
+ };
57
+ case "string":
58
+ case "number":
59
+ case "boolean":
60
+ case "date": return {
61
+ kind: t,
62
+ optional: false
63
+ };
64
+ case "int":
65
+ case "bigint": return {
66
+ kind: "number",
67
+ optional: false
68
+ };
69
+ case "enum": return {
70
+ kind: "enum",
71
+ values: Object.values(def.entries ?? {}).map(String),
72
+ optional: false
73
+ };
74
+ case "literal": {
75
+ const vals = def.values ?? [];
76
+ if (vals.every((v) => typeof v === "boolean")) return {
77
+ kind: "boolean",
78
+ optional: false
79
+ };
80
+ if (vals.every((v) => typeof v === "number")) return {
81
+ kind: "number",
82
+ optional: false
83
+ };
84
+ return {
85
+ kind: "enum",
86
+ values: vals.map(String),
87
+ optional: false
88
+ };
89
+ }
90
+ case "record":
91
+ case "map": return {
92
+ kind: "record",
93
+ optional: false
94
+ };
95
+ case "pipe": return zodToShape(def.out?._zod.def.type === "transform" ? def.in : def.out, depth + 1);
96
+ case "union": return unionShape((def.options ?? []).map((o) => zodToShape(o, depth + 1)));
97
+ case "null":
98
+ case "undefined":
99
+ case "void": return {
100
+ kind: "unknown",
101
+ optional: true
102
+ };
103
+ default: return {
104
+ kind: "unknown",
105
+ optional: false
106
+ };
107
+ }
108
+ }
109
+ /** Unions of objects merge their fields (fields not in every option become optional). */
110
+ function unionShape(options) {
111
+ const optional = options.some((o) => o.optional);
112
+ const real = options.filter((o) => !(o.kind === "unknown" && o.optional));
113
+ if (real.length === 1 && real[0]) return withOptional(real[0], optional);
114
+ if (real.length > 0 && real.every((o) => o.kind === "object")) {
115
+ const fields = {};
116
+ for (const o of real) {
117
+ if (o.kind !== "object") continue;
118
+ for (const [k, v] of Object.entries(o.fields)) fields[k] ??= v;
119
+ }
120
+ for (const k of Object.keys(fields)) {
121
+ const inAll = real.every((o) => o.kind === "object" && Object.hasOwn(o.fields, k));
122
+ const f = fields[k];
123
+ if (f && !inAll) fields[k] = withOptional(f, true);
124
+ }
125
+ return {
126
+ kind: "object",
127
+ fields,
128
+ optional
129
+ };
130
+ }
131
+ if (new Set(real.map((o) => o.kind)).size === 1 && real[0] && real[0].kind !== "object" && real[0].kind !== "array") return withOptional(real[0], optional);
132
+ return {
133
+ kind: "unknown",
134
+ optional
135
+ };
136
+ }
137
+ /** Every variable a schema offers, for UI variable pickers. */
138
+ function shapeVariables(shape, prefix = "", out = []) {
139
+ if (shape.kind === "object") for (const [k, v] of Object.entries(shape.fields)) {
140
+ const path = prefix ? `${prefix}.${k}` : k;
141
+ out.push({
142
+ path,
143
+ kind: v.kind,
144
+ optional: v.optional
145
+ });
146
+ shapeVariables(v, path, out);
147
+ }
148
+ else if (shape.kind === "array") shapeVariables(shape.item, `${prefix}[]`, out);
149
+ return out;
150
+ }
151
+ export { shapeVariables, zodToShape };