@gmickel/gno 1.32.0 → 1.33.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 (33) hide show
  1. package/README.md +11 -2
  2. package/assets/skill/SKILL.md +11 -0
  3. package/assets/skill/cli-reference.md +10 -2
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.32.0.zip → gno-browser-clipper-v1.33.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +5 -1
  8. package/spec/cli.md +16 -1
  9. package/spec/output-schemas/publish-artifact.schema.json +76 -1
  10. package/src/cli/commands/publish.ts +43 -7
  11. package/src/ingestion/strip.ts +152 -26
  12. package/src/publish/artifact-asset-codec.ts +75 -0
  13. package/src/publish/artifact-asset-contract.ts +152 -0
  14. package/src/publish/artifact-asset-parse.ts +401 -0
  15. package/src/publish/artifact-asset-sniff.ts +108 -0
  16. package/src/publish/artifact-asset-validate.ts +209 -0
  17. package/src/publish/artifact-assets.ts +58 -0
  18. package/src/publish/artifact-validation.ts +32 -6
  19. package/src/publish/artifact.ts +50 -3
  20. package/src/publish/attachment-bundle.ts +145 -0
  21. package/src/publish/attachment-discover.ts +203 -0
  22. package/src/publish/attachment-load.ts +133 -0
  23. package/src/publish/attachment-obsidian.ts +45 -0
  24. package/src/publish/attachment-path.ts +334 -0
  25. package/src/publish/attachment-raster.ts +852 -0
  26. package/src/publish/attachment-resolver.ts +280 -0
  27. package/src/publish/attachment-types.ts +54 -0
  28. package/src/publish/encrypted-export.ts +121 -44
  29. package/src/publish/export-attachments.ts +224 -0
  30. package/src/publish/export-service.ts +142 -80
  31. package/src/publish/obsidian-sanitize.ts +121 -13
  32. package/src/serve/routes/api.ts +2 -1
  33. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +0 -1
@@ -2,12 +2,23 @@
2
2
  * Obsidian-aware markdown pre-processor for publish export.
3
3
  *
4
4
  * Strips wikilinks, drops navigation sidebar idioms, removes references to
5
- * private (`_internal/`) paths, and warns on unresolvable image embeds before
6
- * the markdown enters the publish artifact.
5
+ * private (`_internal/`) paths, and resolves/bundles local raster embeds when
6
+ * an attachment context is provided (otherwise drops image embeds with a
7
+ * warning for backward compatibility).
7
8
  *
8
9
  * @module src/publish/obsidian-sanitize
9
10
  */
10
11
 
12
+ import { discoverImageOccurrences } from "./attachment-discover";
13
+ import { safePercentDecode } from "./attachment-path";
14
+ import {
15
+ rewriteAttachmentsInMarkdown,
16
+ type AttachmentDiagnostic,
17
+ type AttachmentResolveContext,
18
+ type AttachmentRewriteResult,
19
+ type PendingAssetPayload,
20
+ } from "./attachment-resolver";
21
+
11
22
  const WIKILINK_INTERNAL_PREFIX = /_internal\//i;
12
23
  const NAV_SIDEBAR_LINE = /^(!?\[\[[^\]]+\]\]\s*\|?\s*)+$/;
13
24
  const IMAGE_EMBED = /!\[\[([^\]]+)\]\]/g;
@@ -16,6 +27,7 @@ const ALIASED_WIKILINK = /\[\[([^\]|]+)\|([^\]]+)\]\]/g;
16
27
  const BARE_WIKILINK = /\[\[([^\]]+)\]\]/g;
17
28
  const TAIL_SEGMENT = /[^/]+$/;
18
29
  const BLOCK_ID_SUFFIX = /#\^?[\w-]+$/;
30
+ const EXTERNAL_DESTINATION = /^(?:\/\/|[a-z][a-z0-9+.-]*:)/iu;
19
31
 
20
32
  export interface SanitizeWarning {
21
33
  kind:
@@ -31,6 +43,13 @@ export interface SanitizeResult {
31
43
  warnings: SanitizeWarning[];
32
44
  }
33
45
 
46
+ export interface PublishSanitizeResult extends SanitizeResult {
47
+ diagnostics: AttachmentDiagnostic[];
48
+ externalCount: number;
49
+ payloads: Map<string, PendingAssetPayload>;
50
+ preDedupRawBytes: number;
51
+ }
52
+
34
53
  const splitFrontmatter = (
35
54
  source: string
36
55
  ): { body: string; frontmatter: string } => {
@@ -61,8 +80,57 @@ const deriveLinkDisplay = (target: string): string => {
61
80
  return tail.trim() || raw;
62
81
  };
63
82
 
64
- export function sanitizeObsidianMarkdown(source: string): SanitizeResult {
65
- const { body, frontmatter } = splitFrontmatter(source);
83
+ const isPrivateImageReference = (sourceRef: string): boolean => {
84
+ const trimmed = sourceRef.trim();
85
+ if (EXTERNAL_DESTINATION.test(trimmed)) {
86
+ return false;
87
+ }
88
+ const decoded = safePercentDecode(trimmed);
89
+ if (decoded === null) {
90
+ return false;
91
+ }
92
+ return (
93
+ decoded
94
+ .split(/[?#]/u, 1)[0]
95
+ ?.split("/")
96
+ .some((segment) => segment.toLowerCase() === "_internal") ?? false
97
+ );
98
+ };
99
+
100
+ /**
101
+ * Remove private image references before attachment resolution can read bytes
102
+ * or replace the authored path with an opaque gno-asset sentinel.
103
+ */
104
+ const stripPrivateImageReferences = (
105
+ body: string
106
+ ): { markdown: string; warnings: SanitizeWarning[] } => {
107
+ const replacements = discoverImageOccurrences(body)
108
+ .filter((occurrence) => isPrivateImageReference(occurrence.sourceRef))
109
+ .map((occurrence) => ({
110
+ detail: occurrence.sourceRef.trim(),
111
+ end: occurrence.end,
112
+ start: occurrence.start,
113
+ }));
114
+ let markdown = body;
115
+ for (const replacement of [...replacements].sort(
116
+ (left, right) => right.start - left.start
117
+ )) {
118
+ markdown =
119
+ markdown.slice(0, replacement.start) + markdown.slice(replacement.end);
120
+ }
121
+ return {
122
+ markdown,
123
+ warnings: replacements.map((replacement) => ({
124
+ detail: replacement.detail,
125
+ kind: "internal-reference-stripped" as const,
126
+ })),
127
+ };
128
+ };
129
+
130
+ const applyWikilinkSanitize = (
131
+ body: string,
132
+ options: { dropImageEmbeds: boolean }
133
+ ): { markdown: string; warnings: SanitizeWarning[] } => {
66
134
  const warnings: SanitizeWarning[] = [];
67
135
  const lines = body.split("\n");
68
136
  const output: string[] = [];
@@ -81,13 +149,15 @@ export function sanitizeObsidianMarkdown(source: string): SanitizeResult {
81
149
 
82
150
  let line = rawLine;
83
151
 
84
- line = line.replace(IMAGE_EMBED, (_match, target: string) => {
85
- warnings.push({
86
- kind: "image-embed-dropped",
87
- detail: target.trim(),
152
+ if (options.dropImageEmbeds) {
153
+ line = line.replace(IMAGE_EMBED, (_match, target: string) => {
154
+ warnings.push({
155
+ kind: "image-embed-dropped",
156
+ detail: target.trim(),
157
+ });
158
+ return "";
88
159
  });
89
- return "";
90
- });
160
+ }
91
161
 
92
162
  line = line.replace(INTERNAL_WIKILINK, (match) => {
93
163
  warnings.push({
@@ -134,9 +204,46 @@ export function sanitizeObsidianMarkdown(source: string): SanitizeResult {
134
204
  output.push(line);
135
205
  }
136
206
 
207
+ return { markdown: output.join("\n"), warnings };
208
+ };
209
+
210
+ /**
211
+ * Legacy sync sanitizer: drops Obsidian image embeds and converts wikilinks.
212
+ * Prefer `sanitizePublishMarkdown` when attachment bundling is enabled.
213
+ */
214
+ export function sanitizeObsidianMarkdown(source: string): SanitizeResult {
215
+ const { body, frontmatter } = splitFrontmatter(source);
216
+ const sanitized = applyWikilinkSanitize(body, { dropImageEmbeds: true });
217
+ return {
218
+ markdown: `${frontmatter}${sanitized.markdown}`,
219
+ warnings: sanitized.warnings,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Parser-aware publish sanitize: resolve/rewrite local rasters in the same
225
+ * pass as Obsidian cleanup (image discovery is not a second independent scan).
226
+ */
227
+ export async function sanitizePublishMarkdown(
228
+ source: string,
229
+ attachmentCtx: AttachmentResolveContext
230
+ ): Promise<PublishSanitizeResult> {
231
+ const { body, frontmatter } = splitFrontmatter(source);
232
+ const privateImages = stripPrivateImageReferences(body);
233
+ const rewritten: AttachmentRewriteResult = await rewriteAttachmentsInMarkdown(
234
+ privateImages.markdown,
235
+ attachmentCtx
236
+ );
237
+ const sanitized = applyWikilinkSanitize(rewritten.markdown, {
238
+ dropImageEmbeds: false,
239
+ });
137
240
  return {
138
- markdown: `${frontmatter}${output.join("\n")}`,
139
- warnings,
241
+ diagnostics: rewritten.diagnostics,
242
+ externalCount: rewritten.externalCount,
243
+ markdown: `${frontmatter}${sanitized.markdown}`,
244
+ payloads: rewritten.payloads,
245
+ preDedupRawBytes: rewritten.preDedupRawBytes,
246
+ warnings: [...privateImages.warnings, ...sanitized.warnings],
140
247
  };
141
248
  }
142
249
 
@@ -159,7 +266,8 @@ export function formatSanitizeWarnings(warnings: SanitizeWarning[]): string[] {
159
266
  }
160
267
 
161
268
  const labels: Record<SanitizeWarning["kind"], string> = {
162
- "image-embed-dropped": "Image embeds dropped (attachments not bundled yet)",
269
+ "image-embed-dropped":
270
+ "Image embeds dropped (unsupported, missing, or unresolved)",
163
271
  "internal-reference-stripped": "Private `_internal/` references stripped",
164
272
  "nav-sidebar-dropped": "Navigation sidebar lines dropped",
165
273
  "wikilink-unresolved":
@@ -1280,7 +1280,7 @@ export async function handlePublishExport(
1280
1280
  }
1281
1281
 
1282
1282
  try {
1283
- const { artifact, warnings } = await exportPublishArtifact({
1283
+ const { artifact, assetSummary, warnings } = await exportPublishArtifact({
1284
1284
  collections: config.collections,
1285
1285
  options: {
1286
1286
  encryptionPassphrase: body.encryptionPassphrase,
@@ -1295,6 +1295,7 @@ export async function handlePublishExport(
1295
1295
 
1296
1296
  return jsonResponse({
1297
1297
  artifact,
1298
+ assetSummary,
1298
1299
  fileName: derivePublishArtifactFilename(artifact),
1299
1300
  uploadUrl: "https://gno.sh/studio",
1300
1301
  warnings,
@@ -1 +0,0 @@
1
- cb5c4bba8cf1252da4e4fc89f1dc01a8df306851cccb50f636d89a6ef3133cff gno-browser-clipper-v1.32.0.zip