@atmo-dev/kit 0.7.1 → 0.8.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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rich text parsing without Svelte or a DOM. Parse on the server and pass the document to
3
+ * `RichText` so the Markdown parser stays out of client bundles.
4
+ */
5
+ import { parseMarkdown } from './richtext/markdown.js';
6
+ import { parsePlainText } from './richtext/plain.js';
7
+ export function parseRichText(text, options = {}) {
8
+ return options.markdown ? parseMarkdown(text, options) : parsePlainText(text, options);
9
+ }
10
+ export { parseMarkdown, parsePlainText };
11
+ export { richTextToPlainText, safeWebUrl } from './richtext/document.js';
12
+ export { detectTokens } from './richtext/detect.js';
@@ -1,4 +1,4 @@
1
- import { tokenize } from '@atcute/bluesky-richtext-parser';
1
+ import { detectTokens } from '../richtext/detect.js';
2
2
  import { safeWebUrl } from './richtext.js';
3
3
  import { socialHashtagUrl, socialProfileUrl } from './urls.js';
4
4
  /** Only web URLs; never accept executable schemes or disguised credentials. */
@@ -13,7 +13,7 @@ export function safeProfileUrl(value) {
13
13
  return url.username || url.password ? undefined : href;
14
14
  }
15
15
  export function profileDescriptionSegments(text, options = {}) {
16
- return Array.from(tokenize(text), (token) => ({
16
+ return Array.from(detectTokens(text), (token) => ({
17
17
  text: token.raw,
18
18
  href: token.type === 'autolink'
19
19
  ? safeProfileUrl(token.url)
@@ -1,5 +1,6 @@
1
1
  import type { JSONContent } from '@tiptap/core';
2
- import type { PreferredSocialClient } from './preferred-client.js';
2
+ import type { RichTextLinkOptions } from '../richtext/document.js';
3
+ export { safeWebUrl } from '../richtext/document.js';
3
4
  export type Facet = {
4
5
  index: {
5
6
  byteStart: number;
@@ -12,24 +13,20 @@ export type Facet = {
12
13
  tag?: string;
13
14
  }[];
14
15
  };
15
- export declare function safeWebUrl(url: unknown): string | undefined;
16
16
  export declare function textToDoc(text: string): JSONContent;
17
17
  /** Serialize mention nodes first, then detect links/tags using UTF-8 byte offsets. */
18
18
  export declare function editorJsonToBlueskyPost(doc: JSONContent): {
19
19
  text: string;
20
20
  facets: Facet[];
21
21
  };
22
- export type RichTextOptions = {
23
- socialClient?: PreferredSocialClient | null;
24
- /** App-owned mention destination; defaults to the selected social client. */
25
- mentionUrl?: (identifier: string) => string;
22
+ /** @deprecated Use `RichTextLinkOptions` with `detectLinks` on `RichText` or `parseRichText`. */
23
+ export type RichTextOptions = RichTextLinkOptions & {
26
24
  /** Detect plain-text links, mentions and hashtags only when there are no facets. */
27
25
  detectLinks?: boolean;
28
26
  };
27
+ /** @deprecated `RichText` now renders a `RichTextDocument`. */
29
28
  export type RichTextSegment = {
30
29
  text: string;
31
30
  href?: string;
32
31
  };
33
- /** Plain text and validated links, never HTML or Markdown. Invalid facets leave text intact. */
34
- export declare function richTextSegments(text: string, facets?: unknown, options?: RichTextOptions): RichTextSegment[];
35
32
  export declare function graphemeLength(text: string): number;
@@ -1,19 +1,5 @@
1
- import { tokenize } from '@atcute/bluesky-richtext-parser';
2
- import { isDid } from '@atcute/lexicons/syntax';
3
- import { socialHashtagUrl, socialProfileUrl } from './urls.js';
4
- export function safeWebUrl(url) {
5
- if (typeof url !== 'string' || !url)
6
- return undefined;
7
- try {
8
- const parsed = new URL(url);
9
- return ['https:', 'http:'].includes(parsed.protocol) && !parsed.username && !parsed.password
10
- ? parsed.href
11
- : undefined;
12
- }
13
- catch {
14
- return undefined;
15
- }
16
- }
1
+ import { detectTokens } from '../richtext/detect.js';
2
+ export { safeWebUrl } from '../richtext/document.js';
17
3
  export function textToDoc(text) {
18
4
  return {
19
5
  type: 'doc',
@@ -52,7 +38,7 @@ export function editorJsonToBlueskyPost(doc) {
52
38
  });
53
39
  const facets = [...mentions];
54
40
  let byteStart = 0;
55
- for (const token of tokenize(text)) {
41
+ for (const token of detectTokens(text)) {
56
42
  const byteEnd = byteStart + bytes(token.raw);
57
43
  if (!mentions.some((m) => byteStart < m.index.byteEnd && byteEnd > m.index.byteStart)) {
58
44
  if (token.type === 'autolink')
@@ -70,83 +56,6 @@ export function editorJsonToBlueskyPost(doc) {
70
56
  }
71
57
  return { text, facets: facets.sort((a, b) => a.index.byteStart - b.index.byteStart) };
72
58
  }
73
- function record(value) {
74
- return value !== null && typeof value === 'object' && !Array.isArray(value);
75
- }
76
- /** Only app-relative paths or HTTP(S), including for app-owned mention callbacks. */
77
- function safeMentionUrl(identifier, options) {
78
- const href = options.mentionUrl?.(identifier) ?? socialProfileUrl(identifier, options.socialClient);
79
- if (typeof href !== 'string' ||
80
- Array.from(href).some((char) => char === '\\' || char.charCodeAt(0) <= 0x20 || char.charCodeAt(0) === 0x7f))
81
- return undefined;
82
- if (href.startsWith('/') && !href.startsWith('//'))
83
- return href;
84
- return safeWebUrl(href);
85
- }
86
- function featureUrl(feature, options) {
87
- if (!record(feature))
88
- return undefined;
89
- switch (feature.$type) {
90
- case 'app.bsky.richtext.facet#mention':
91
- return typeof feature.did === 'string' && isDid(feature.did)
92
- ? safeMentionUrl(feature.did, options)
93
- : undefined;
94
- case 'app.bsky.richtext.facet#link':
95
- return safeWebUrl(feature.uri);
96
- case 'app.bsky.richtext.facet#tag':
97
- return typeof feature.tag === 'string' && feature.tag && feature.tag.isWellFormed()
98
- ? socialHashtagUrl(feature.tag, options.socialClient)
99
- : undefined;
100
- }
101
- }
102
- /** Plain text and validated links, never HTML or Markdown. Invalid facets leave text intact. */
103
- export function richTextSegments(text, facets = [], options = {}) {
104
- if ((!Array.isArray(facets) || !facets.length) && options.detectLinks) {
105
- return Array.from(tokenize(text), (token) => ({
106
- text: token.raw,
107
- href: token.type === 'autolink'
108
- ? safeWebUrl(token.url)
109
- : token.type === 'mention'
110
- ? safeMentionUrl(token.handle, options)
111
- : token.type === 'topic'
112
- ? socialHashtagUrl(token.name, options.socialClient)
113
- : undefined
114
- }));
115
- }
116
- const encoded = new TextEncoder().encode(text);
117
- const decoder = new TextDecoder();
118
- const segments = [];
119
- const valid = [];
120
- // UTF-8 continuation bytes cannot start/end a facet.
121
- const boundary = (offset) => offset === encoded.length || (encoded[offset] & 0xc0) !== 0x80;
122
- for (const facet of Array.isArray(facets) ? facets : []) {
123
- if (!record(facet) || !record(facet.index) || !Array.isArray(facet.features))
124
- continue;
125
- const { byteStart, byteEnd } = facet.index;
126
- if (typeof byteStart !== 'number' ||
127
- typeof byteEnd !== 'number' ||
128
- !Number.isSafeInteger(byteStart) ||
129
- !Number.isSafeInteger(byteEnd) ||
130
- byteStart < 0 ||
131
- byteEnd <= byteStart ||
132
- byteEnd > encoded.length ||
133
- !boundary(byteStart) ||
134
- !boundary(byteEnd))
135
- continue;
136
- valid.push({ byteStart, byteEnd, features: facet.features });
137
- }
138
- let cursor = 0;
139
- for (const { byteStart, byteEnd, features } of valid.sort((a, b) => a.byteStart - b.byteStart)) {
140
- if (byteStart < cursor)
141
- continue;
142
- const href = features.map((feature) => featureUrl(feature, options)).find(Boolean);
143
- segments.push({ text: decoder.decode(encoded.slice(cursor, byteStart)) });
144
- segments.push({ text: decoder.decode(encoded.slice(byteStart, byteEnd)), href });
145
- cursor = byteEnd;
146
- }
147
- segments.push({ text: decoder.decode(encoded.slice(cursor)) });
148
- return segments;
149
- }
150
59
  export function graphemeLength(text) {
151
60
  return Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(text))
152
61
  .length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atmo-dev/kit",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "The shared UI foundation for Atmo apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,6 +56,10 @@
56
56
  "types": "./dist/media.d.ts",
57
57
  "default": "./dist/media.js"
58
58
  },
59
+ "./richtext": {
60
+ "types": "./dist/richtext.d.ts",
61
+ "default": "./dist/richtext.js"
62
+ },
59
63
  "./social": {
60
64
  "types": "./dist/social.d.ts",
61
65
  "default": "./dist/social.js"
@@ -68,11 +72,11 @@
68
72
  "./theme.css": "./dist/theme.css"
69
73
  },
70
74
  "peerDependencies": {
71
- "svelte": ">=5.48.0 <6",
72
- "tailwindcss": "^4.0.0",
73
75
  "maplibre-gl": "^6.16.0",
76
+ "plyr": "^3.8.5",
77
+ "svelte": ">=5.48.0 <6",
74
78
  "svelte-maplibre-gl": "^2.2.1",
75
- "plyr": "^3.8.5"
79
+ "tailwindcss": "^4.0.0"
76
80
  },
77
81
  "peerDependenciesMeta": {
78
82
  "maplibre-gl": {
@@ -87,10 +91,6 @@
87
91
  },
88
92
  "devDependencies": {
89
93
  "@axe-core/playwright": "^4.14.0",
90
- "axe-core": "^4.14.0",
91
- "maplibre-gl": "^6.16.0",
92
- "svelte-maplibre-gl": "^2.2.1",
93
- "plyr": "^3.8.5",
94
94
  "@changesets/cli": "^2.31.1",
95
95
  "@eslint/js": "^10.0.1",
96
96
  "@playwright/test": "^1.60.0",
@@ -102,19 +102,24 @@
102
102
  "@tailwindcss/typography": "^0.5.19",
103
103
  "@tailwindcss/vite": "^4.3.0",
104
104
  "@types/canvas-confetti": "^1.9.0",
105
+ "@types/mdast": "^4.0.4",
105
106
  "@types/node": "^24",
106
107
  "@vitest/browser-playwright": "^4.1.8",
108
+ "axe-core": "^4.14.0",
107
109
  "eslint": "^10.4.1",
108
110
  "eslint-config-prettier": "^10.1.8",
109
111
  "eslint-plugin-svelte": "^3.19.0",
110
112
  "globals": "^17.6.0",
113
+ "maplibre-gl": "^6.16.0",
111
114
  "playwright": "^1.60.0",
115
+ "plyr": "^3.8.5",
112
116
  "prettier": "^3.8.3",
113
117
  "prettier-plugin-svelte": "^4.1.0",
114
118
  "prettier-plugin-tailwindcss": "^0.8.0",
115
119
  "publint": "^0.3.21",
116
120
  "svelte": "^5.57.1",
117
121
  "svelte-check": "^4.6.0",
122
+ "svelte-maplibre-gl": "^2.2.1",
118
123
  "tailwindcss": "^4.3.0",
119
124
  "typescript": "^6.0.3",
120
125
  "typescript-eslint": "^8.60.1",
@@ -131,16 +136,11 @@
131
136
  },
132
137
  "dependencies": {
133
138
  "@atcute/bluesky-richtext-parser": "^2.1.2",
139
+ "@atcute/lexicons": "^2.1.1",
134
140
  "@atcute/tid": "^1.1.4",
141
+ "@fontsource-variable/figtree": "^5.3.0",
135
142
  "@internationalized/date": "^3.12.4",
136
143
  "@number-flow/svelte": "^0.4.2",
137
- "esm-env": "^1.2.2",
138
- "h3-js": "^4.5.0",
139
- "i18n-iso-countries": "^7.14.0",
140
- "svelte-boring-avatars": "^1.2.6",
141
- "valibot": "^1.5.0",
142
- "@atcute/lexicons": "^2.1.1",
143
- "@fontsource-variable/figtree": "^5.3.0",
144
144
  "@sketchyicons/svelte": "^0.5.0",
145
145
  "@tiptap/core": "^3.32.0",
146
146
  "@tiptap/extension-document": "^3.32.0",
@@ -153,8 +153,16 @@
153
153
  "bits-ui": "^2.20.2",
154
154
  "canvas-confetti": "^1.9.4",
155
155
  "clsx": "^2.1.1",
156
+ "esm-env": "^1.2.2",
157
+ "h3-js": "^4.5.0",
156
158
  "hls.js": "^1.7.3",
157
- "tailwind-merge": "^3.7.0"
159
+ "i18n-iso-countries": "^7.14.0",
160
+ "mdast-util-from-markdown": "^2.1.0",
161
+ "mdast-util-gfm-strikethrough": "^2.0.1",
162
+ "micromark-extension-gfm-strikethrough": "^2.1.0",
163
+ "svelte-boring-avatars": "^1.2.6",
164
+ "tailwind-merge": "^3.7.0",
165
+ "valibot": "^1.5.0"
158
166
  },
159
167
  "scripts": {
160
168
  "dev": "vite dev",