@charlescms/astro 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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/SECURITY.md +77 -0
  4. package/THIRD_PARTY_NOTICES.md +56 -0
  5. package/connector/worker.js +505 -0
  6. package/connector/wrangler.toml +15 -0
  7. package/package.json +92 -0
  8. package/scripts/check-licenses.js +45 -0
  9. package/scripts/check-package.js +62 -0
  10. package/scripts/setup.js +719 -0
  11. package/scripts/update-vendored-site.js +71 -0
  12. package/src/admin.astro +314 -0
  13. package/src/analyzer.js +639 -0
  14. package/src/asset-images.js +130 -0
  15. package/src/astro-frontmatter.js +17 -0
  16. package/src/boot.js +35 -0
  17. package/src/client.js +347 -0
  18. package/src/connector-client.js +185 -0
  19. package/src/content-bridge.js +162 -0
  20. package/src/content-panel.js +440 -0
  21. package/src/data-analyzer.js +304 -0
  22. package/src/edit-affordance.js +463 -0
  23. package/src/editor-styles.js +243 -0
  24. package/src/element-editor.js +355 -0
  25. package/src/fields.js +6 -0
  26. package/src/frontmatter.js +153 -0
  27. package/src/ids.js +20 -0
  28. package/src/index.js +681 -0
  29. package/src/js-ast.js +140 -0
  30. package/src/markdown-analyzer.js +95 -0
  31. package/src/media-preview.js +58 -0
  32. package/src/panel-manager.js +133 -0
  33. package/src/publishing.js +457 -0
  34. package/src/rich-text-editor.js +209 -0
  35. package/src/routes.js +21 -0
  36. package/src/runtime-controller.js +206 -0
  37. package/src/sanitize.js +150 -0
  38. package/src/section-editor.js +437 -0
  39. package/src/source-edit.js +310 -0
  40. package/src/source-map-runtime.js +184 -0
  41. package/src/staged-panel.js +145 -0
  42. package/src/toolbar.js +128 -0
  43. package/src/versions-panel.js +112 -0
@@ -0,0 +1,153 @@
1
+ import { load } from "js-yaml";
2
+ import { getFrontmatter } from "./analyzer.js";
3
+ import { createEntryId } from "./ids.js";
4
+
5
+ /**
6
+ * Collect editable YAML frontmatter scalars with their exact source positions.
7
+ *
8
+ * Includes nested keys (`author.name`, `category.title`) common in content
9
+ * collections. The edit model stays bulletproof: only single-line scalars are
10
+ * located, and only the value's exact bytes are replaced — never restructuring
11
+ * YAML. Indentation gives the key path; an unresolvable path is skipped, so a
12
+ * tracking mistake can only drop a field, never corrupt the file.
13
+ *
14
+ * @param {string} source Full file contents (frontmatter is extracted internally).
15
+ * @param {string} file File path, used to build stable entry ids.
16
+ * @returns {{entries: Object}} Map of entry id → frontmatter field descriptor.
17
+ */
18
+ export function collectFrontmatterFields(source, file) {
19
+ const frontmatter = getFrontmatter(source);
20
+ if (!frontmatter) return { entries: {} };
21
+
22
+ let parsed;
23
+ try {
24
+ parsed = load(frontmatter.value);
25
+ } catch {
26
+ return { entries: {} };
27
+ }
28
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
29
+ return { entries: {} };
30
+ }
31
+
32
+ const entries = {};
33
+ let lineOffset = 0;
34
+ // Stack of open mapping keys (a key with no inline value), so an indented line
35
+ // knows its parents. Each entry is { indent, key }.
36
+ const stack = [];
37
+ for (const lineWithEnding of frontmatter.value.match(/[^\r\n]*(?:\r\n|\n|\r|$)/g) || []) {
38
+ if (!lineWithEnding) continue;
39
+ const line = lineWithEnding.replace(/\r\n$|\n$|\r$/, "");
40
+ const match = line.match(/^([ \t]*)([A-Za-z_][\w.-]*):[ \t]*(.*)$/);
41
+ if (!match) {
42
+ lineOffset += lineWithEnding.length;
43
+ continue;
44
+ }
45
+
46
+ const indent = match[1].length;
47
+ const key = match[2];
48
+ const rawValue = match[3];
49
+ // Close any sibling/deeper mappings before placing this key in the path.
50
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
51
+ const path = [...stack.map((level) => level.key), key];
52
+
53
+ // A key with no inline value opens a nested mapping (or a list); remember it
54
+ // so its children resolve, and move on.
55
+ if (rawValue.trim() === "") {
56
+ stack.push({ indent, key });
57
+ lineOffset += lineWithEnding.length;
58
+ continue;
59
+ }
60
+
61
+ const parsedValue = resolvePath(parsed, path);
62
+ const scalar = locateScalar(rawValue);
63
+ const valueType = classifyValue(parsedValue);
64
+ if (!scalar || !valueType) {
65
+ lineOffset += lineWithEnding.length;
66
+ continue;
67
+ }
68
+
69
+ const rawValueStart = line.length - rawValue.length;
70
+ const start = frontmatter.offset + lineOffset + rawValueStart + scalar.start;
71
+ const operation = {
72
+ kind: "frontmatter",
73
+ name: path.join("."),
74
+ start,
75
+ end: start + scalar.value.length,
76
+ oldValue: scalar.value,
77
+ quote: scalar.quote,
78
+ valueType
79
+ };
80
+ const id = createEntryId(file, [operation]);
81
+ entries[id] = { id, file, tag: "frontmatter", operations: [operation] };
82
+ lineOffset += lineWithEnding.length;
83
+ }
84
+
85
+ return { entries };
86
+ }
87
+
88
+ // Walk a key path (["author","name"]) through plain nested objects only. Anything
89
+ // that isn't an own key of an object along the way yields undefined, so the line
90
+ // is treated as non-editable rather than guessed.
91
+ function resolvePath(root, path) {
92
+ let value = root;
93
+ for (const segment of path) {
94
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Object.hasOwn(value, segment)) {
95
+ return undefined;
96
+ }
97
+ value = value[segment];
98
+ }
99
+ return value;
100
+ }
101
+
102
+ function locateScalar(valueSource) {
103
+ const leading = valueSource.match(/^[ \t]*/)?.[0].length || 0;
104
+ const value = valueSource.slice(leading);
105
+ if (!value || /^[|>]/.test(value) || /^[&*]/.test(value)) return null;
106
+
107
+ if (value[0] === '"' || value[0] === "'") {
108
+ const quote = value[0];
109
+ const end = findClosingQuote(value, quote);
110
+ if (end === -1 || !isOnlyComment(value.slice(end + 1))) return null;
111
+ return {
112
+ quote,
113
+ start: leading + 1,
114
+ value: value.slice(1, end)
115
+ };
116
+ }
117
+
118
+ const comment = value.search(/[ \t]+#/);
119
+ const raw = (comment === -1 ? value : value.slice(0, comment)).trimEnd();
120
+ if (!raw || /^(?:\[|\{)/.test(raw) || /:\s*$/.test(raw)) return null;
121
+ return { quote: "", start: leading, value: raw };
122
+ }
123
+
124
+ function findClosingQuote(value, quote) {
125
+ for (let index = 1; index < value.length; index++) {
126
+ if (value[index] !== quote) continue;
127
+ if (quote === "'" && value[index + 1] === "'") {
128
+ index++;
129
+ continue;
130
+ }
131
+ if (quote === '"' && isEscaped(value, index)) continue;
132
+ return index;
133
+ }
134
+ return -1;
135
+ }
136
+
137
+ function isEscaped(value, index) {
138
+ let slashes = 0;
139
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor--) slashes++;
140
+ return slashes % 2 === 1;
141
+ }
142
+
143
+ function isOnlyComment(value) {
144
+ return /^(?:[ \t]*(?:#.*)?)?$/.test(value);
145
+ }
146
+
147
+ function classifyValue(value) {
148
+ if (typeof value === "string") return "string";
149
+ if (typeof value === "number" && Number.isFinite(value)) return "number";
150
+ if (typeof value === "boolean") return "boolean";
151
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return "date";
152
+ return null;
153
+ }
package/src/ids.js ADDED
@@ -0,0 +1,20 @@
1
+ // Produces stable source-map IDs from file paths and edit operations. The same
2
+ // source yields the same ID in development and production without shared state.
3
+ export function createEntryId(file, operations) {
4
+ const value = [
5
+ file,
6
+ ...operations.map((operation) => `${operation.kind}:${operation.name}:${operation.start}:${operation.end}:${operation.oldValue}`)
7
+ ].join("\n");
8
+ return `cms_${stableHash(value)}`;
9
+ }
10
+
11
+ function stableHash(value) {
12
+ let hash = 0xcbf29ce484222325n;
13
+ const prime = 0x100000001b3n;
14
+ const bytes = new TextEncoder().encode(value);
15
+ for (const byte of bytes) {
16
+ hash ^= BigInt(byte);
17
+ hash = BigInt.asUintN(64, hash * prime);
18
+ }
19
+ return hash.toString(16).padStart(16, "0");
20
+ }