@valbuild/shared 0.150.13 → 0.150.17

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.150.17
4
+
5
+ ### Patch Changes
6
+
7
+ - [#992](https://github.com/valbuild/val/pull/992) [`ea3bf9b`](https://github.com/valbuild/val/commit/ea3bf9bbacd22bbb5485fd798d6b3a2d5916be81) Thanks [@freekh](https://github.com/freekh)! - Content analytics now counts on self-hosted sites (Custom hosting), not only on sites Val hosts.
8
+
9
+ A self-hosted app's own Val server now hands each analytics beacon on to Val's ingest, which counts it. To turn it on:
10
+
11
+ - Run the app with Val's content service: `VAL_API_KEY`, `VAL_SECRET` and `VAL_PROJECT` set. Most deployed apps already do.
12
+ - Set `VAL_ANALYTICS=true`. In TanStack Start, read it in your layout's loader (`{ enabled: process.env.VAL_ANALYTICS === "true" }`) and pass the result to `<ValProvider analytics>`. A self-hosted site has no platform to pass it for you.
13
+ - Switch analytics on for the project in Val's admin. While it is off there, the script on your pages is told so and stops sending.
14
+
15
+ The server forwards only same-origin beacons from a browser. It passes on the visitor's User-Agent and, where your host provides one (Vercel, Cloudflare or CloudFront), their country. It never passes on their IP address. If the api key is not accepted, the server logs one warning naming `VAL_API_KEY` and `VAL_PROJECT`. Local development (fs mode) works as before and counts nothing.
16
+
17
+ `VAL_ANALYTICS_INGEST_URL` overrides where beacons are sent. You will normally not need it.
18
+
19
+ ## 0.150.16
20
+
21
+ ### Patch Changes
22
+
23
+ - [#940](https://github.com/valbuild/val/pull/940) [`3ac24d4`](https://github.com/valbuild/val/commit/3ac24d46691c646dd5d2789cf2e1d6e2734195d3) Thanks [@freekh](https://github.com/freekh)! - Picking a language in the Studio now scopes the whole Studio to it.
24
+
25
+ - The Pages tree, the Data list, lists inside a field and search show only content in that language, plus content in no language (settings, authors, images), which every language shares. A page is in a language when its route names it (`s.router(router, { locale: … }, item)`) or its content has an `s.locale()` field.
26
+ - Picking a language while you are on something in another one moves you: to the same entry in the new language where Val knows it (a record keyed by `s.locale()`, or a route that differs only in its language segment), otherwise to the nearest parent that is not in another language, otherwise to the Studio's start screen with nothing selected.
27
+ - A link or a reference to something in another language still opens it, and that one item is listed while you are on it.
28
+ - Publish, Discard all and the error count are not filtered: they act on every language.
29
+
30
+ - [#969](https://github.com/valbuild/val/pull/969) [`7db39ef`](https://github.com/valbuild/val/commit/7db39ef722b4f743e6974effd5fedfadc76e6544) Thanks [@freekh](https://github.com/freekh)! - **Sketchpad**: work out a page's content and rough layout with the assistant before anyone builds it.
31
+
32
+ A sketchpad is a document of headings, text, boxes and columns, where you edit the text in place. A box can show an image or a video from a link: paste one, or drag an image in from a web page. The assistant sees the same sketch as text in one strict format, so what it reads is what is on screen. It has tools to list, create, read and write sketchpads, and **Send to AI** asks it to read the one you are on.
33
+
34
+ - Sketchpads live on the server, apart from any chat or page. They outlast the chat they started in, and the page a sketchpad is about can be changed. A sketchpad can be archived and reopened.
35
+ - Open one from a page's menu (**Sketch changes**), from the top bar, or when the assistant offers one.
36
+ - On a desktop the page, the sketchpad and the chat sit side by side, the chat docked beside them. On a phone a sketchpad can go full screen, with the page one tap away and the buttons at the bottom.
37
+ - In local development sketchpads are stored in `.val/sketchpads/`. A hosted project needs the content host's sketchpad API; without it the Studio does not offer sketchpads.
38
+
3
39
  ## 0.150.13
4
40
 
5
41
  ### Patch Changes
@@ -82,6 +82,43 @@ export declare const AnalyticsBeacon: z.ZodObject<{
82
82
  viewed: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
83
83
  }, z.core.$strip>;
84
84
  export type AnalyticsBeacon = z.infer<typeof AnalyticsBeacon>;
85
+ /**
86
+ * A Sketchpad, as the server keeps it: a rough page written in the strict
87
+ * format of `sketchpad/sketchFormat.ts`, owned by the project and nothing
88
+ * else. Not by an AI session -- the assistant that made it remembers its id,
89
+ * and any chat can read it again -- and not by a page: `page` is a label for
90
+ * which page it is about, and can be changed or cleared.
91
+ *
92
+ * `version` goes up by one on every change, and a change names the version
93
+ * it was made against, so two writers cannot overwrite each other unseen.
94
+ */
95
+ export declare const Sketchpad: z.ZodObject<{
96
+ id: z.ZodString;
97
+ page: z.ZodNullable<z.ZodString>;
98
+ title: z.ZodNullable<z.ZodString>;
99
+ text: z.ZodString;
100
+ version: z.ZodNumber;
101
+ archived: z.ZodBoolean;
102
+ createdAt: z.ZodString;
103
+ updatedAt: z.ZodString;
104
+ createdBy: z.ZodNullable<z.ZodString>;
105
+ updatedBy: z.ZodNullable<z.ZodString>;
106
+ }, z.core.$strip>;
107
+ export type SketchpadT = z.infer<typeof Sketchpad>;
108
+ export declare const SketchpadSummary: z.ZodObject<{
109
+ id: z.ZodString;
110
+ title: z.ZodNullable<z.ZodString>;
111
+ page: z.ZodNullable<z.ZodString>;
112
+ version: z.ZodNumber;
113
+ createdAt: z.ZodString;
114
+ updatedAt: z.ZodString;
115
+ archived: z.ZodBoolean;
116
+ createdBy: z.ZodNullable<z.ZodString>;
117
+ updatedBy: z.ZodNullable<z.ZodString>;
118
+ }, z.core.$strip>;
119
+ export type SketchpadSummaryT = z.infer<typeof SketchpadSummary>;
120
+ /** The most text a sketchpad holds, in UTF-8 bytes: a page, not a site. */
121
+ export declare const SKETCHPAD_MAX_TEXT_LENGTH: number;
85
122
  export declare const Api: {
86
123
  "/draft/enable": {
87
124
  GET: {
@@ -139,6 +176,13 @@ export declare const Api: {
139
176
  POST: {
140
177
  req: {
141
178
  body: z.ZodUnknown;
179
+ headers: {
180
+ "sec-fetch-site": z.ZodOptional<z.ZodString>;
181
+ "user-agent": z.ZodOptional<z.ZodString>;
182
+ "x-vercel-ip-country": z.ZodOptional<z.ZodString>;
183
+ "cf-ipcountry": z.ZodOptional<z.ZodString>;
184
+ "cloudfront-viewer-country": z.ZodOptional<z.ZodString>;
185
+ };
142
186
  };
143
187
  res: z.ZodUnion<readonly [z.ZodObject<{
144
188
  status: z.ZodLiteral<200>;
@@ -150,6 +194,11 @@ export declare const Api: {
150
194
  json: z.ZodObject<{
151
195
  message: z.ZodString;
152
196
  }, z.core.$strip>;
197
+ }, z.core.$strip>, z.ZodObject<{
198
+ status: z.ZodLiteral<410>;
199
+ json: z.ZodObject<{
200
+ message: z.ZodString;
201
+ }, z.core.$strip>;
153
202
  }, z.core.$strip>]>;
154
203
  };
155
204
  };
@@ -700,6 +749,221 @@ export declare const Api: {
700
749
  }, z.core.$strip>;
701
750
  };
702
751
  };
752
+ /**
753
+ * The project's sketchpads: list them, or make one. See `Sketchpad`.
754
+ * Making one answers with its id, which is what an assistant keeps to
755
+ * read and change it later.
756
+ */
757
+ "/sketchpads": {
758
+ GET: {
759
+ req: {
760
+ query: {
761
+ /** `exclude` (the default), `include`, or `only`. */
762
+ archived: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>>;
763
+ /** Only those about this page. */
764
+ page: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>>;
765
+ };
766
+ cookies: {
767
+ val_session: z.ZodOptional<z.ZodString>;
768
+ };
769
+ };
770
+ res: z.ZodUnion<readonly [z.ZodObject<{
771
+ status: z.ZodLiteral<401>;
772
+ json: z.ZodObject<{
773
+ message: z.ZodString;
774
+ }, z.core.$strip>;
775
+ }, z.core.$strip>, z.ZodObject<{
776
+ status: z.ZodLiteral<400>;
777
+ json: z.ZodObject<{
778
+ message: z.ZodString;
779
+ }, z.core.$strip>;
780
+ }, z.core.$strip>, z.ZodObject<{
781
+ status: z.ZodLiteral<500>;
782
+ json: z.ZodObject<{
783
+ message: z.ZodString;
784
+ }, z.core.$strip>;
785
+ }, z.core.$strip>, z.ZodObject<{
786
+ status: z.ZodLiteral<200>;
787
+ json: z.ZodObject<{
788
+ sketchpads: z.ZodArray<z.ZodObject<{
789
+ id: z.ZodString;
790
+ title: z.ZodNullable<z.ZodString>;
791
+ page: z.ZodNullable<z.ZodString>;
792
+ version: z.ZodNumber;
793
+ createdAt: z.ZodString;
794
+ updatedAt: z.ZodString;
795
+ archived: z.ZodBoolean;
796
+ createdBy: z.ZodNullable<z.ZodString>;
797
+ updatedBy: z.ZodNullable<z.ZodString>;
798
+ }, z.core.$strip>>;
799
+ }, z.core.$strip>;
800
+ }, z.core.$strip>]>;
801
+ };
802
+ POST: {
803
+ req: {
804
+ body: z.ZodObject<{
805
+ text: z.ZodString;
806
+ page: z.ZodNullable<z.ZodString>;
807
+ }, z.core.$strip>;
808
+ cookies: {
809
+ val_session: z.ZodOptional<z.ZodString>;
810
+ };
811
+ };
812
+ res: z.ZodUnion<readonly [z.ZodObject<{
813
+ status: z.ZodLiteral<401>;
814
+ json: z.ZodObject<{
815
+ message: z.ZodString;
816
+ }, z.core.$strip>;
817
+ }, z.core.$strip>, z.ZodObject<{
818
+ status: z.ZodLiteral<400>;
819
+ json: z.ZodObject<{
820
+ message: z.ZodString;
821
+ errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
822
+ line: z.ZodNumber;
823
+ message: z.ZodString;
824
+ }, z.core.$strip>>>;
825
+ }, z.core.$strip>;
826
+ }, z.core.$strip>, z.ZodObject<{
827
+ status: z.ZodLiteral<500>;
828
+ json: z.ZodObject<{
829
+ message: z.ZodString;
830
+ }, z.core.$strip>;
831
+ }, z.core.$strip>, z.ZodObject<{
832
+ status: z.ZodLiteral<201>;
833
+ json: z.ZodObject<{
834
+ sketchpad: z.ZodObject<{
835
+ id: z.ZodString;
836
+ page: z.ZodNullable<z.ZodString>;
837
+ title: z.ZodNullable<z.ZodString>;
838
+ text: z.ZodString;
839
+ version: z.ZodNumber;
840
+ archived: z.ZodBoolean;
841
+ createdAt: z.ZodString;
842
+ updatedAt: z.ZodString;
843
+ createdBy: z.ZodNullable<z.ZodString>;
844
+ updatedBy: z.ZodNullable<z.ZodString>;
845
+ }, z.core.$strip>;
846
+ }, z.core.$strip>;
847
+ }, z.core.$strip>]>;
848
+ };
849
+ };
850
+ /** One sketchpad: read it, or change its text, its page or whether it is archived. */
851
+ "/sketchpad": {
852
+ GET: {
853
+ req: {
854
+ query: {
855
+ id: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
856
+ };
857
+ cookies: {
858
+ val_session: z.ZodOptional<z.ZodString>;
859
+ };
860
+ };
861
+ res: z.ZodUnion<readonly [z.ZodObject<{
862
+ status: z.ZodLiteral<401>;
863
+ json: z.ZodObject<{
864
+ message: z.ZodString;
865
+ }, z.core.$strip>;
866
+ }, z.core.$strip>, z.ZodObject<{
867
+ status: z.ZodLiteral<404>;
868
+ json: z.ZodObject<{
869
+ message: z.ZodString;
870
+ }, z.core.$strip>;
871
+ }, z.core.$strip>, z.ZodObject<{
872
+ status: z.ZodLiteral<500>;
873
+ json: z.ZodObject<{
874
+ message: z.ZodString;
875
+ }, z.core.$strip>;
876
+ }, z.core.$strip>, z.ZodObject<{
877
+ status: z.ZodLiteral<200>;
878
+ json: z.ZodObject<{
879
+ sketchpad: z.ZodObject<{
880
+ id: z.ZodString;
881
+ page: z.ZodNullable<z.ZodString>;
882
+ title: z.ZodNullable<z.ZodString>;
883
+ text: z.ZodString;
884
+ version: z.ZodNumber;
885
+ archived: z.ZodBoolean;
886
+ createdAt: z.ZodString;
887
+ updatedAt: z.ZodString;
888
+ createdBy: z.ZodNullable<z.ZodString>;
889
+ updatedBy: z.ZodNullable<z.ZodString>;
890
+ }, z.core.$strip>;
891
+ }, z.core.$strip>;
892
+ }, z.core.$strip>]>;
893
+ };
894
+ PATCH: {
895
+ req: {
896
+ body: z.ZodObject<{
897
+ id: z.ZodString;
898
+ baseVersion: z.ZodNumber;
899
+ text: z.ZodOptional<z.ZodString>;
900
+ page: z.ZodOptional<z.ZodNullable<z.ZodString>>;
901
+ archived: z.ZodOptional<z.ZodBoolean>;
902
+ }, z.core.$strip>;
903
+ cookies: {
904
+ val_session: z.ZodOptional<z.ZodString>;
905
+ };
906
+ };
907
+ res: z.ZodUnion<readonly [z.ZodObject<{
908
+ status: z.ZodLiteral<401>;
909
+ json: z.ZodObject<{
910
+ message: z.ZodString;
911
+ }, z.core.$strip>;
912
+ }, z.core.$strip>, z.ZodObject<{
913
+ status: z.ZodLiteral<404>;
914
+ json: z.ZodObject<{
915
+ message: z.ZodString;
916
+ }, z.core.$strip>;
917
+ }, z.core.$strip>, z.ZodObject<{
918
+ status: z.ZodLiteral<400>;
919
+ json: z.ZodObject<{
920
+ message: z.ZodString;
921
+ errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
922
+ line: z.ZodNumber;
923
+ message: z.ZodString;
924
+ }, z.core.$strip>>>;
925
+ }, z.core.$strip>;
926
+ }, z.core.$strip>, z.ZodObject<{
927
+ status: z.ZodLiteral<409>;
928
+ json: z.ZodObject<{
929
+ message: z.ZodString;
930
+ sketchpad: z.ZodObject<{
931
+ id: z.ZodString;
932
+ page: z.ZodNullable<z.ZodString>;
933
+ title: z.ZodNullable<z.ZodString>;
934
+ text: z.ZodString;
935
+ version: z.ZodNumber;
936
+ archived: z.ZodBoolean;
937
+ createdAt: z.ZodString;
938
+ updatedAt: z.ZodString;
939
+ createdBy: z.ZodNullable<z.ZodString>;
940
+ updatedBy: z.ZodNullable<z.ZodString>;
941
+ }, z.core.$strip>;
942
+ }, z.core.$strip>;
943
+ }, z.core.$strip>, z.ZodObject<{
944
+ status: z.ZodLiteral<500>;
945
+ json: z.ZodObject<{
946
+ message: z.ZodString;
947
+ }, z.core.$strip>;
948
+ }, z.core.$strip>, z.ZodObject<{
949
+ status: z.ZodLiteral<200>;
950
+ json: z.ZodObject<{
951
+ sketchpad: z.ZodObject<{
952
+ id: z.ZodString;
953
+ page: z.ZodNullable<z.ZodString>;
954
+ title: z.ZodNullable<z.ZodString>;
955
+ text: z.ZodString;
956
+ version: z.ZodNumber;
957
+ archived: z.ZodBoolean;
958
+ createdAt: z.ZodString;
959
+ updatedAt: z.ZodString;
960
+ createdBy: z.ZodNullable<z.ZodString>;
961
+ updatedBy: z.ZodNullable<z.ZodString>;
962
+ }, z.core.$strip>;
963
+ }, z.core.$strip>;
964
+ }, z.core.$strip>]>;
965
+ };
966
+ };
703
967
  "/upload/patches": {
704
968
  POST: {
705
969
  req: {
@@ -1,2 +1,2 @@
1
- export declare const toolNames: readonly ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys"];
1
+ export declare const toolNames: readonly ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys", "list_sketchpads", "create_sketchpad", "read_sketchpad", "write_sketchpad"];
2
2
  export type ToolName = (typeof toolNames)[number];
@@ -21,6 +21,7 @@ export * from "./getNextAppRouterSourceFolder.js";
21
21
  export * from "./routeValidation.js";
22
22
  export * from "./resolveSchemaSourceFixes.js";
23
23
  export * from "./localeAt.js";
24
+ export * from "./localeRelocation.js";
24
25
  export * from "./getErrorMessageFromUnknownJson.js";
25
26
  export * from "./zod/SerializedSchema.js";
26
27
  export * from "./valCanvasProtocol.js";
@@ -30,6 +31,7 @@ export * from "./pageScreenshot.js";
30
31
  export * from "./quickPreview.js";
31
32
  export * from "./emptyOf.js";
32
33
  export * from "./aiTools/toolNames.js";
34
+ export * from "./sketchpad/sketchFormat.js";
33
35
  export * from "./aiTools/aiImageToolPatches.js";
34
36
  export * from "./aiTools/aiSourceToolPatches.js";
35
37
  export * from "./validation/partitionValidationErrors.js";
@@ -1,4 +1,4 @@
1
- import { type SerializedEnumSchema, type SerializedSchema, type SourcePath } from "@valbuild/core";
1
+ import { type Json, type JsonObject, type ModuleFilePath, type SerializedEnumSchema, type SerializedSchema, type SourcePath } from "@valbuild/core";
2
2
  import { type SchemaSourceSnapshot } from "./resolveSchemaSourceFixes.js";
3
3
  /**
4
4
  * Which of the project's languages governs a path, or `null` for none.
@@ -18,6 +18,52 @@ import { type SchemaSourceSnapshot } from "./resolveSchemaSourceFixes.js";
18
18
  * in most projects, and for every project that has not declared any languages.
19
19
  */
20
20
  export declare function localeAt(path: SourcePath, snapshot: SchemaSourceSnapshot): string | null;
21
+ /**
22
+ * Where the scope governing a path opens, and how.
23
+ *
24
+ * - `field`: the node `depth` segments down has a locale field. `depth` 0 is the
25
+ * module root.
26
+ * - `record-key`: segment `depth` is a key of a locale-keyed record, which sits
27
+ * `depth` segments down.
28
+ * - `route-key`: segment `depth` is a route whose parameters name the language,
29
+ * in the router `depth` segments down (in practice the module root).
30
+ *
31
+ * The opener is what decides where an editor goes when the Studio's language
32
+ * changes under them — the same entry in another language for a key, the
33
+ * parent for a field. See `relocateForLocale`.
34
+ */
35
+ export type LocaleScope = {
36
+ locale: string;
37
+ depth: number;
38
+ } & ({
39
+ kind: "field";
40
+ } | {
41
+ kind: "record-key";
42
+ keySchema: SerializedSchema;
43
+ } | {
44
+ kind: "route-key";
45
+ router: SerializedSchema & {
46
+ type: "record";
47
+ };
48
+ });
49
+ /**
50
+ * The scope that governs a path, read from that module's schema and source
51
+ * alone, or `null` where none does.
52
+ *
53
+ * {@link localeAt} for a caller that already has the module in hand — the
54
+ * Studio, which has the project's languages from its own provider and would
55
+ * otherwise have to assemble a snapshot of every module to ask about one.
56
+ */
57
+ export declare function localeScopeAt(path: SourcePath, schema: SerializedSchema, source: Json, available: readonly string[]): LocaleScope | null;
58
+ /**
59
+ * The language a whole module is in: the one its root's locale field names, or
60
+ * `null` where its root has none.
61
+ *
62
+ * The only way a module is in one language as a whole — a locale-keyed record
63
+ * or a router at the root is every one of its languages at once. Needs no path,
64
+ * so a list of modules can ask it of each.
65
+ */
66
+ export declare function localeOfModuleRoot(schema: SerializedSchema, source: Json, available: readonly string[]): string | null;
21
67
  /**
22
68
  * The language a locale schema's value MEANS, or `null` if it means none of
23
69
  * the project's.
@@ -44,3 +90,42 @@ export declare function enumLocaleTag(schema: SerializedEnumSchema, value: strin
44
90
  * one.
45
91
  */
46
92
  export declare function isLocaleSchema(schema: SerializedSchema): boolean;
93
+ /**
94
+ * Whether a router's URLs name the language each page is in — one of its route
95
+ * parameters is a locale (`s.router(router, { locale: … }, item)`).
96
+ */
97
+ export declare function hasLocaleRouteParam(schema: SerializedSchema): boolean;
98
+ /**
99
+ * The language a router's key is in, where one of its route parameters is a
100
+ * locale — `/nb/blog/hei` is `nb-NO` when `locale` maps `nb` to it.
101
+ *
102
+ * Answered from the key alone, with no content read: which is what lets the
103
+ * Studio's page tree filter every page of a site without loading one of them.
104
+ */
105
+ export declare function localeOfRouteKey(schema: SerializedSchema & {
106
+ type: "record";
107
+ }, moduleFilePath: ModuleFilePath, key: string, available: readonly string[]): string | null;
108
+ /**
109
+ * The route parameters a key gives its router, and which of them is the
110
+ * language — or `null` where the router has no locale parameter or the key is
111
+ * not one of its URLs.
112
+ */
113
+ export declare function localeParamOfRouteKey(schema: SerializedSchema & {
114
+ type: "record";
115
+ }, moduleFilePath: ModuleFilePath, key: string): {
116
+ name: string;
117
+ schema: SerializedSchema;
118
+ value: string | null;
119
+ values: Record<string, string | null>;
120
+ } | null;
121
+ /** The child of a source value at a segment, or `null` where there is none. */
122
+ export declare function childSource(source: Json, segment: string): Json;
123
+ /**
124
+ * Whether `source` is a JSON object rather than an array or a primitive.
125
+ *
126
+ * A type predicate rather than the inline checks, because `Array.isArray` does
127
+ * NOT narrow `JsonArray` out of `Json` — it is `readonly Json[]`, and the
128
+ * built-in guard only narrows mutable arrays. Without this, every read of a
129
+ * property would need an assertion.
130
+ */
131
+ export declare function isJsonObject(source: Json): source is JsonObject;
@@ -0,0 +1,46 @@
1
+ import { type Json, type SerializedSchema, type SourcePath } from "@valbuild/core";
2
+ /**
3
+ * Where an editor goes when the Studio's language changes under them.
4
+ *
5
+ * - `stay`: the path is in that language, or in none.
6
+ * - `path`: the same entry in that language (`counterpart`), or the nearest
7
+ * parent that is not in another language (`parent`).
8
+ * - `home`: neither exists, so the editor goes to the Studio's start, with
9
+ * nothing selected.
10
+ */
11
+ export type LocaleRelocation = {
12
+ type: "stay";
13
+ } | {
14
+ type: "path";
15
+ path: SourcePath;
16
+ reason: "counterpart" | "parent";
17
+ } | {
18
+ type: "home";
19
+ };
20
+ /**
21
+ * Where to move a path to when the Studio is switched to `locale`.
22
+ *
23
+ * The rule, in the order it is tried:
24
+ *
25
+ * 1. **The same entry in `locale`, where Val KNOWS it.** That is two cases:
26
+ * a locale-keyed record, where the counterpart of `"en-US"` is the
27
+ * `"nb-NO"` key of the same record; and a router whose URLs name their
28
+ * language, where `/about` and `/nb/about` differ only in it. Only a key
29
+ * that exists is taken, and the rest of the path is kept as far as it
30
+ * still exists, so an editor on a field lands on that field.
31
+ * 2. **Otherwise the nearest parent that is not in another language.** An
32
+ * English post in a list of posts gives the list, now showing Norwegian.
33
+ * 3. **Otherwise the Studio's start**, which is `home`: nothing selected. That
34
+ * is where an object at the module root opened the scope (the whole module
35
+ * is in another language), and where the parent would be a router: a list
36
+ * of pages is not a page, and the Pages tree is how an editor sees one.
37
+ *
38
+ * Not the site's root page, and not any other page: which language a page is
39
+ * in is the page's own business, and `/` can be in the language the editor
40
+ * just left. The start screen is in no language.
41
+ *
42
+ * Never a guess. Two pages whose URLs differ in more than the language — a
43
+ * Norwegian slug for a Norwegian page — are not known to be translations of
44
+ * each other, so that is `home` rather than whichever page looks closest.
45
+ */
46
+ export declare function relocateForLocale(path: SourcePath, locale: string, schema: SerializedSchema, source: Json, available: readonly string[]): LocaleRelocation;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * A page sketch: a page written as indented text, with four primitives.
3
+ *
4
+ * ```
5
+ * # Our people text (a heading: #, ## or ###)
6
+ * We are 40 people in Oslo. text (a paragraph)
7
+ * [image: Team photo] placeholder: [kind] or [kind: label]
8
+ * [hero: Roof](image: /a.jpg) a placeholder with an image (or video) in it
9
+ * icon hero M3 5h18v14H3z how every [hero] box is drawn: SVG path data
10
+ * row its children side by side
11
+ * col 2 its children stacked; 2 shares of the row
12
+ * ## Design
13
+ * col
14
+ * [button: Join us]
15
+ * ```
16
+ *
17
+ * It is a scratch pad, not a layout language: it says WHAT is on a page and
18
+ * roughly WHERE, so an assistant and a person can agree on a page's content
19
+ * before anyone worries about how it looks. The person never sees this text,
20
+ * only the page it describes; the assistant never sees the page, only this.
21
+ * So the two have to be the same thing, and the format is STRICT: there is
22
+ * exactly one way to write what is on the screen. `checkSketch` is that
23
+ * rule, and the server refuses a write that breaks it, with the line to fix.
24
+ *
25
+ * Parsing is still forgiving, so a sketch that breaks the rule shows what it
26
+ * can rather than nothing; that is for reading, never for accepting.
27
+ *
28
+ * A placeholder's kind is any word, and nothing here knows what one means. The
29
+ * assistant that writes the sketch also draws each kind's icon, as path data
30
+ * in a 24 by 24 box, so a kind nobody thought of looks as deliberate as an
31
+ * image. Media put in a box is written into the text too, so the assistant
32
+ * that builds the page reads it exactly as the person left it.
33
+ */
34
+ export type SketchNode = SketchRow | SketchCol | SketchText | SketchPlaceholder;
35
+ export type SketchRow = {
36
+ type: "row";
37
+ line: number;
38
+ children: SketchNode[];
39
+ };
40
+ export type SketchCol = {
41
+ type: "col";
42
+ line: number;
43
+ /** Shares of the enclosing row this column takes. */
44
+ weight: number;
45
+ children: SketchNode[];
46
+ };
47
+ export type SketchText = {
48
+ type: "text";
49
+ line: number;
50
+ /** 0 is a paragraph; 1-3 are headings. */
51
+ level: 0 | 1 | 2 | 3;
52
+ text: string;
53
+ };
54
+ export type SketchPlaceholder = {
55
+ type: "placeholder";
56
+ line: number;
57
+ kind: string;
58
+ label: string | null;
59
+ media: SketchMedia | null;
60
+ };
61
+ export type SketchMedia = {
62
+ type: "image" | "video";
63
+ src: string;
64
+ };
65
+ export type SketchError = {
66
+ /** 1-based, like an editor's gutter. */
67
+ line: number;
68
+ message: string;
69
+ };
70
+ export type ParsedSketch = {
71
+ /** The page itself is a column. */
72
+ blocks: SketchNode[];
73
+ /**
74
+ * SVG path data by kind, lower case. Drawn as a 24 by 24 line icon: stroked
75
+ * in the text colour, not filled.
76
+ */
77
+ icons: Record<string, string>;
78
+ errors: SketchError[];
79
+ };
80
+ export declare function parseSketch(source: string): ParsedSketch;
81
+ /**
82
+ * The line a text block is written as. Line breaks become spaces, and a
83
+ * paragraph that would read as something else is kept as text with `\`.
84
+ */
85
+ export declare function formatSketchText(level: SketchText["level"], text: string): string;
86
+ /** The line a placeholder is written as. */
87
+ export declare function formatSketchBox(placeholder: Pick<SketchPlaceholder, "kind" | "label" | "media">): string;
88
+ /**
89
+ * The ONE way of writing `blocks` and `icons`: what `checkSketch` holds every
90
+ * other text to, and what the Studio's editor writes (its own serializer is
91
+ * tested against this one). Two spaces per level; an empty row gets an empty
92
+ * col, so it has somewhere to type; a col outside a row is written as its
93
+ * contents, which is all it shows; the icons last, after one blank line, and
94
+ * no blank line anywhere else; a final newline. Empty text is not written --
95
+ * it is not on the page.
96
+ */
97
+ export declare function formatSketch(blocks: SketchNode[], icons: Record<string, string>): string;
98
+ /**
99
+ * What a list names a sketch by: its first top-level heading, the biggest
100
+ * one first. Null for a sketch with no heading, which a list then names by
101
+ * its page.
102
+ */
103
+ export declare function sketchTitle(blocks: SketchNode[]): string | null;
104
+ /**
105
+ * Everything wrong with `source`, or nothing when it is a sketch exactly as
106
+ * `formatSketch` writes it.
107
+ *
108
+ * Strict on purpose. The person sees the page and the assistant sees the
109
+ * text, so a text with two spellings of one page -- a tab or two spaces, an
110
+ * icon line here or there, a blank line or none -- is a text the assistant
111
+ * cannot trust to be what is on the screen, and that would show it a change
112
+ * nobody made the next time the Studio writes it back. Holding every write
113
+ * to the one spelling rules all of that out at once.
114
+ *
115
+ * The parse errors come first; past those, the first line that is spelled
116
+ * differently, with how it should be spelled.
117
+ */
118
+ export declare function checkSketch(source: string): SketchError[];