@aglyn/aglyn 1.0.0-beta.231 → 1.0.0-beta.232
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/package.json +11 -11
- package/src/lib/app-utils/account-acquisition.d.ts +15 -2
- package/src/lib/app-utils/account-acquisition.js +20 -1
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/artifact-list-queries.d.ts +35 -0
- package/src/lib/app-utils/artifact-list-queries.js +203 -0
- package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
- package/src/lib/app-utils/business-profile.d.ts +171 -0
- package/src/lib/app-utils/business-profile.js +277 -0
- package/src/lib/app-utils/business-profile.js.map +1 -0
- package/src/lib/app-utils/child-contract.d.ts +4 -1
- package/src/lib/app-utils/child-contract.js +5 -2
- package/src/lib/app-utils/child-contract.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +5 -0
- package/src/lib/app-utils/console-routes.js +1 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/content-schema-type.d.ts +9 -0
- package/src/lib/app-utils/content-schema-type.js +4 -0
- package/src/lib/app-utils/content-schema-type.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +41 -5
- package/src/lib/app-utils/docs-help.generated.js +83 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +181 -13
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
- package/src/lib/app-utils/entry-list-declaration.js +172 -0
- package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
- package/src/lib/app-utils/first-version-seed.d.ts +19 -0
- package/src/lib/app-utils/first-version-seed.js +53 -0
- package/src/lib/app-utils/first-version-seed.js.map +1 -0
- package/src/lib/app-utils/local-business.d.ts +8 -0
- package/src/lib/app-utils/local-business.js +4 -0
- package/src/lib/app-utils/local-business.js.map +1 -1
- package/src/lib/app-utils/page-markdown.js +3 -0
- package/src/lib/app-utils/page-markdown.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +19 -0
- package/src/lib/plugin-manager/feature-plugins.js +8 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +105 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
- package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
- package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
- package/src/lib/plugin-manager/plugin-product-catalog.d.ts +5 -0
- package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
- package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
- package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
- package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
- package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
- package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
- package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
7
|
+
import type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
8
|
+
export declare const ENTRY_LIST_FILTER_FIELDS: readonly ListFilterField[];
|
|
9
|
+
/** How each entry field reads — as a hidden column's header, and on a chip. */
|
|
10
|
+
export declare const ENTRY_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
|
|
11
|
+
/**
|
|
12
|
+
* Everything a FILTERED or searched entries list can ask of Firestore
|
|
13
|
+
* (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's
|
|
14
|
+
* token array, and the orders those predicates may be combined with.
|
|
15
|
+
*
|
|
16
|
+
* Three orders, each justified by the four composites it costs (one per
|
|
17
|
+
* predicate field — status, category, author, title tokens):
|
|
18
|
+
*
|
|
19
|
+
* Published, newest first the list's default; mandatory.
|
|
20
|
+
* Published, oldest first the same column reversed — the first posts in
|
|
21
|
+
* a category, the earliest of an author's.
|
|
22
|
+
* Updated, newest first what was just edited, which is the question an
|
|
23
|
+
* editor brings to a filtered list.
|
|
24
|
+
*
|
|
25
|
+
* Title and Status, and Updated oldest first, would cost four more each and
|
|
26
|
+
* are offered on the UNFILTERED list only, where they cost none.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ENTRY_LIST_QUERY: ListQueryDeclaration;
|
|
29
|
+
/** The stored statuses the Status filter offers, in the order it lists them. */
|
|
30
|
+
export declare const ENTRY_STATUS_OPTIONS: ReadonlyArray<{
|
|
31
|
+
value: string;
|
|
32
|
+
label: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/ import { ENTRY_PUBLISH_SORT_FIELD } from "./collection-entry-date.js";
|
|
6
|
+
/**
|
|
7
|
+
* The array the quick search and the title filter read: the same key
|
|
8
|
+
* `ENTRY_TITLE_TOKENS_FIELD` names in `content-query-fields.ts`, written by
|
|
9
|
+
* `entryTitleSearchFields`. Named here rather than imported because that
|
|
10
|
+
* module reaches the author model, which the native apps never read.
|
|
11
|
+
*/ const ENTRY_TITLE_TOKENS_FIELD = 'titleTokens';
|
|
12
|
+
/*
|
|
13
|
+
* WHAT A COLLECTION'S ENTRY LIST ASKS FIRESTORE (AGL-3321), as one pure
|
|
14
|
+
* module: the fields its Filters panel accepts, their headers, the orders a
|
|
15
|
+
* filtered list may take, and the statuses. The console's Content page plans
|
|
16
|
+
* its queries from here (`entry-list-query.ts`, `list-filters.ts`), and so do
|
|
17
|
+
* the native apps, through the contracts codegen (AGL-3668).
|
|
18
|
+
*/ /*
|
|
19
|
+
* Content entries (`hosts/{hostId}/collections/{collectionId}/entries`).
|
|
20
|
+
*
|
|
21
|
+
* EVERY clause below is on the Firestore query together (AGL-3321), composed
|
|
22
|
+
* by `planListQuery` through `ENTRY_LIST_QUERY` in
|
|
23
|
+
* this module, which also holds the orders a filtered list may take. Nothing is matched over the rows a page loaded.
|
|
24
|
+
*
|
|
25
|
+
* The equalities — status, category, author — and the title search each need
|
|
26
|
+
* one composite per filtered order (index merging serves every combination of
|
|
27
|
+
* them), which is what keeps this list's filtered orders to three. The two
|
|
28
|
+
* dates are ranges over fields those orders already walk, so they cost no
|
|
29
|
+
* index of their own; a range leads the order, and so reads with ONE ordered
|
|
30
|
+
* query rather than the two-segment walk: every entry it matches carries the
|
|
31
|
+
* field it ranges over.
|
|
32
|
+
*
|
|
33
|
+
* `status` matches the stored word. An entry with no `status` at all — only a
|
|
34
|
+
* hand-written import bundle produces one — matches none of the three, and is
|
|
35
|
+
* still on the unfiltered list.
|
|
36
|
+
*
|
|
37
|
+
* `categoryId` and `authorId` match the stable ids the entry editor writes.
|
|
38
|
+
* An entry that still carries only the legacy free-typed `category` or
|
|
39
|
+
* `authorName` matches no id until one is picked for it in the editor.
|
|
40
|
+
*
|
|
41
|
+
* `title` is WORD-level, over `titleTokens` (`entryTitleSearchFields`): the
|
|
42
|
+
* quick search reads the same array, and a query holds one array clause, so
|
|
43
|
+
* the two do not combine.
|
|
44
|
+
*
|
|
45
|
+
* `publishedAt` is the Published column, and ranges over the date that column
|
|
46
|
+
* shows — `publishSortAt`, the scheduled date while an entry waits and the
|
|
47
|
+
* published date after (AGL-3323). A draft has neither and matches no range.
|
|
48
|
+
*/ export const ENTRY_LIST_FILTER_FIELDS = [
|
|
49
|
+
{
|
|
50
|
+
column: 'title',
|
|
51
|
+
kind: 'text',
|
|
52
|
+
path: 'title',
|
|
53
|
+
tokensPath: 'titleTokens',
|
|
54
|
+
operators: [
|
|
55
|
+
'contains'
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
column: 'status',
|
|
60
|
+
kind: 'exact',
|
|
61
|
+
path: 'status',
|
|
62
|
+
operators: [
|
|
63
|
+
'equals',
|
|
64
|
+
'isAnyOf'
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
column: 'categoryId',
|
|
69
|
+
kind: 'exact',
|
|
70
|
+
path: 'categoryId',
|
|
71
|
+
operators: [
|
|
72
|
+
'equals',
|
|
73
|
+
'isAnyOf'
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
column: 'authorId',
|
|
78
|
+
kind: 'exact',
|
|
79
|
+
path: 'authorId',
|
|
80
|
+
operators: [
|
|
81
|
+
'equals',
|
|
82
|
+
'isAnyOf'
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
column: 'publishedAt',
|
|
87
|
+
kind: 'date',
|
|
88
|
+
path: 'publishSortAt',
|
|
89
|
+
operators: [
|
|
90
|
+
'is',
|
|
91
|
+
'after',
|
|
92
|
+
'onOrAfter',
|
|
93
|
+
'before',
|
|
94
|
+
'onOrBefore'
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
column: 'updatedAt',
|
|
99
|
+
kind: 'date',
|
|
100
|
+
path: 'updatedAt',
|
|
101
|
+
operators: [
|
|
102
|
+
'is',
|
|
103
|
+
'after',
|
|
104
|
+
'onOrAfter',
|
|
105
|
+
'before',
|
|
106
|
+
'onOrBefore'
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
/** How each entry field reads — as a hidden column's header, and on a chip. */ export const ENTRY_LIST_FILTER_HEADERS = {
|
|
111
|
+
title: 'Title',
|
|
112
|
+
status: 'Status',
|
|
113
|
+
categoryId: 'Category',
|
|
114
|
+
authorId: 'Author',
|
|
115
|
+
publishedAt: 'Published',
|
|
116
|
+
updatedAt: 'Updated'
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Everything a FILTERED or searched entries list can ask of Firestore
|
|
120
|
+
* (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's
|
|
121
|
+
* token array, and the orders those predicates may be combined with.
|
|
122
|
+
*
|
|
123
|
+
* Three orders, each justified by the four composites it costs (one per
|
|
124
|
+
* predicate field — status, category, author, title tokens):
|
|
125
|
+
*
|
|
126
|
+
* Published, newest first the list's default; mandatory.
|
|
127
|
+
* Published, oldest first the same column reversed — the first posts in
|
|
128
|
+
* a category, the earliest of an author's.
|
|
129
|
+
* Updated, newest first what was just edited, which is the question an
|
|
130
|
+
* editor brings to a filtered list.
|
|
131
|
+
*
|
|
132
|
+
* Title and Status, and Updated oldest first, would cost four more each and
|
|
133
|
+
* are offered on the UNFILTERED list only, where they cost none.
|
|
134
|
+
*/ export const ENTRY_LIST_QUERY = {
|
|
135
|
+
fields: ENTRY_LIST_FILTER_FIELDS,
|
|
136
|
+
sorts: [
|
|
137
|
+
{
|
|
138
|
+
path: ENTRY_PUBLISH_SORT_FIELD,
|
|
139
|
+
direction: 'desc',
|
|
140
|
+
column: 'publishedAt'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
path: ENTRY_PUBLISH_SORT_FIELD,
|
|
144
|
+
direction: 'asc',
|
|
145
|
+
column: 'publishedAt'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: 'updatedAt',
|
|
149
|
+
direction: 'desc',
|
|
150
|
+
column: 'updatedAt'
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
search: {
|
|
154
|
+
tokensPath: ENTRY_TITLE_TOKENS_FIELD
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/** The stored statuses the Status filter offers, in the order it lists them. */ export const ENTRY_STATUS_OPTIONS = [
|
|
158
|
+
{
|
|
159
|
+
value: 'draft',
|
|
160
|
+
label: 'Draft'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
value: 'published',
|
|
164
|
+
label: 'Published'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
value: 'scheduled',
|
|
168
|
+
label: 'Scheduled'
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=entry-list-declaration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entry-list-declaration.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { ENTRY_PUBLISH_SORT_FIELD } from './collection-entry-date'\n\n/**\n * The array the quick search and the title filter read: the same key\n * `ENTRY_TITLE_TOKENS_FIELD` names in `content-query-fields.ts`, written by\n * `entryTitleSearchFields`. Named here rather than imported because that\n * module reaches the author model, which the native apps never read.\n */\nconst ENTRY_TITLE_TOKENS_FIELD = 'titleTokens'\n\n/*\n * WHAT A COLLECTION'S ENTRY LIST ASKS FIRESTORE (AGL-3321), as one pure\n * module: the fields its Filters panel accepts, their headers, the orders a\n * filtered list may take, and the statuses. The console's Content page plans\n * its queries from here (`entry-list-query.ts`, `list-filters.ts`), and so do\n * the native apps, through the contracts codegen (AGL-3668).\n */\n\n/*\n * Content entries (`hosts/{hostId}/collections/{collectionId}/entries`).\n *\n * EVERY clause below is on the Firestore query together (AGL-3321), composed\n * by `planListQuery` through `ENTRY_LIST_QUERY` in\n * this module, which also holds the orders a filtered list may take. Nothing is matched over the rows a page loaded.\n *\n * The equalities — status, category, author — and the title search each need\n * one composite per filtered order (index merging serves every combination of\n * them), which is what keeps this list's filtered orders to three. The two\n * dates are ranges over fields those orders already walk, so they cost no\n * index of their own; a range leads the order, and so reads with ONE ordered\n * query rather than the two-segment walk: every entry it matches carries the\n * field it ranges over.\n *\n * `status` matches the stored word. An entry with no `status` at all — only a\n * hand-written import bundle produces one — matches none of the three, and is\n * still on the unfiltered list.\n *\n * `categoryId` and `authorId` match the stable ids the entry editor writes.\n * An entry that still carries only the legacy free-typed `category` or\n * `authorName` matches no id until one is picked for it in the editor.\n *\n * `title` is WORD-level, over `titleTokens` (`entryTitleSearchFields`): the\n * quick search reads the same array, and a query holds one array clause, so\n * the two do not combine.\n *\n * `publishedAt` is the Published column, and ranges over the date that column\n * shows — `publishSortAt`, the scheduled date while an entry waits and the\n * published date after (AGL-3323). A draft has neither and matches no range.\n */\nexport const ENTRY_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'title',\n kind: 'text',\n path: 'title',\n tokensPath: 'titleTokens',\n operators: ['contains'],\n },\n {\n column: 'status',\n kind: 'exact',\n path: 'status',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'categoryId',\n kind: 'exact',\n path: 'categoryId',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'authorId',\n kind: 'exact',\n path: 'authorId',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'publishedAt',\n kind: 'date',\n path: 'publishSortAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n]\n\n/** How each entry field reads — as a hidden column's header, and on a chip. */\nexport const ENTRY_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n title: 'Title',\n status: 'Status',\n categoryId: 'Category',\n authorId: 'Author',\n publishedAt: 'Published',\n updatedAt: 'Updated',\n}\n\n/**\n * Everything a FILTERED or searched entries list can ask of Firestore\n * (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's\n * token array, and the orders those predicates may be combined with.\n *\n * Three orders, each justified by the four composites it costs (one per\n * predicate field — status, category, author, title tokens):\n *\n * Published, newest first the list's default; mandatory.\n * Published, oldest first the same column reversed — the first posts in\n * a category, the earliest of an author's.\n * Updated, newest first what was just edited, which is the question an\n * editor brings to a filtered list.\n *\n * Title and Status, and Updated oldest first, would cost four more each and\n * are offered on the UNFILTERED list only, where they cost none.\n */\nexport const ENTRY_LIST_QUERY: ListQueryDeclaration = {\n fields: ENTRY_LIST_FILTER_FIELDS,\n sorts: [\n { path: ENTRY_PUBLISH_SORT_FIELD, direction: 'desc', column: 'publishedAt' },\n { path: ENTRY_PUBLISH_SORT_FIELD, direction: 'asc', column: 'publishedAt' },\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt' },\n ],\n search: { tokensPath: ENTRY_TITLE_TOKENS_FIELD },\n}\n\n/** The stored statuses the Status filter offers, in the order it lists them. */\nexport const ENTRY_STATUS_OPTIONS: ReadonlyArray<{\n value: string\n label: string\n}> = [\n { value: 'draft', label: 'Draft' },\n { value: 'published', label: 'Published' },\n { value: 'scheduled', label: 'Scheduled' },\n]\n"],"names":["ENTRY_PUBLISH_SORT_FIELD","ENTRY_TITLE_TOKENS_FIELD","ENTRY_LIST_FILTER_FIELDS","column","kind","path","tokensPath","operators","ENTRY_LIST_FILTER_HEADERS","title","status","categoryId","authorId","publishedAt","updatedAt","ENTRY_LIST_QUERY","fields","sorts","direction","search","ENTRY_STATUS_OPTIONS","value","label"],"mappings":"AAAA;;;;CAIC,GAID,SAASA,wBAAwB,QAAQ,6BAAyB;AAElE;;;;;CAKC,GACD,MAAMC,2BAA2B;AAEjC;;;;;;CAMC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,MAAMC,2BAAuD;IAClE;QACEC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;CACD,CAAA;AAED,6EAA6E,GAC7E,OAAO,MAAMC,4BAA8D;IACzEC,OAAO;IACPC,QAAQ;IACRC,YAAY;IACZC,UAAU;IACVC,aAAa;IACbC,WAAW;AACb,EAAC;AAED;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,mBAAyC;IACpDC,QAAQd;IACRe,OAAO;QACL;YAAEZ,MAAML;YAA0BkB,WAAW;YAAQf,QAAQ;QAAc;QAC3E;YAAEE,MAAML;YAA0BkB,WAAW;YAAOf,QAAQ;QAAc;QAC1E;YAAEE,MAAM;YAAaa,WAAW;YAAQf,QAAQ;QAAY;KAC7D;IACDgB,QAAQ;QAAEb,YAAYL;IAAyB;AACjD,EAAC;AAED,8EAA8E,GAC9E,OAAO,MAAMmB,uBAGR;IACH;QAAEC,OAAO;QAASC,OAAO;IAAQ;IACjC;QAAED,OAAO;QAAaC,OAAO;IAAY;IACzC;QAAED,OAAO;QAAaC,OAAO;IAAY;CAC1C,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/** The back-pointer key a version of each parent kind carries. */
|
|
7
|
+
export declare const VERSION_PARENT_KEYS: Readonly<Record<string, string>>;
|
|
8
|
+
/** A canvas with nothing on it but its root. */
|
|
9
|
+
export declare function rootOnlyCanvas(): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* The first version's document for [parentId] of [kind], from the parent's
|
|
12
|
+
* stored fields with its node map already DECODED. Null for a kind with no
|
|
13
|
+
* versions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function firstVersionSeed(kind: string, parentId: string, hostId: string, parent: {
|
|
16
|
+
nodes?: Record<string, unknown> | null;
|
|
17
|
+
rootId?: unknown;
|
|
18
|
+
props?: unknown;
|
|
19
|
+
}): Record<string, unknown> | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/ /*
|
|
7
|
+
* A resource's FIRST version, seeded from the resource itself (AGL-3668).
|
|
8
|
+
*
|
|
9
|
+
* A reusable component is created with its design on its own document and no
|
|
10
|
+
* version, and the console's "Open Besigner" mints the first version from
|
|
11
|
+
* that document before it opens it: the decoded node map (or a root-only
|
|
12
|
+
* canvas when it is empty, never `{}`, which renders as "Invalid node"), the
|
|
13
|
+
* canvas root, and the declared properties. The native apps cannot decode a
|
|
14
|
+
* compressed node map, so `/api/hosts/versions` builds the same seed on the
|
|
15
|
+
* server from the stored document (`seedFromParent`), and the node map never
|
|
16
|
+
* crosses the wire. This is that seed, as one pure rule both sides share.
|
|
17
|
+
*/ const CANVAS_ROOT_ID = '_@_';
|
|
18
|
+
/** The back-pointer key a version of each parent kind carries. */ export const VERSION_PARENT_KEYS = {
|
|
19
|
+
screen: 'screenId',
|
|
20
|
+
layout: 'layoutId',
|
|
21
|
+
component: 'componentId',
|
|
22
|
+
form: 'formId'
|
|
23
|
+
};
|
|
24
|
+
/** A canvas with nothing on it but its root. */ export function rootOnlyCanvas() {
|
|
25
|
+
return {
|
|
26
|
+
[CANVAS_ROOT_ID]: {
|
|
27
|
+
$id: CANVAS_ROOT_ID,
|
|
28
|
+
componentId: 'div',
|
|
29
|
+
nodes: []
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The first version's document for [parentId] of [kind], from the parent's
|
|
35
|
+
* stored fields with its node map already DECODED. Null for a kind with no
|
|
36
|
+
* versions.
|
|
37
|
+
*/ export function firstVersionSeed(kind, parentId, hostId, parent) {
|
|
38
|
+
const backPointer = VERSION_PARENT_KEYS[kind];
|
|
39
|
+
if (!backPointer) return null;
|
|
40
|
+
const nodes = parent.nodes && Object.keys(parent.nodes).length ? parent.nodes : rootOnlyCanvas();
|
|
41
|
+
return _extends({
|
|
42
|
+
[backPointer]: parentId,
|
|
43
|
+
hostId,
|
|
44
|
+
displayName: 'Initial version',
|
|
45
|
+
nodes
|
|
46
|
+
}, typeof parent.rootId === 'string' && parent.rootId ? {
|
|
47
|
+
rootId: parent.rootId
|
|
48
|
+
} : {}, Array.isArray(parent.props) && parent.props.length ? {
|
|
49
|
+
props: parent.props
|
|
50
|
+
} : {});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=first-version-seed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/first-version-seed.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/*\n * A resource's FIRST version, seeded from the resource itself (AGL-3668).\n *\n * A reusable component is created with its design on its own document and no\n * version, and the console's \"Open Besigner\" mints the first version from\n * that document before it opens it: the decoded node map (or a root-only\n * canvas when it is empty, never `{}`, which renders as \"Invalid node\"), the\n * canvas root, and the declared properties. The native apps cannot decode a\n * compressed node map, so `/api/hosts/versions` builds the same seed on the\n * server from the stored document (`seedFromParent`), and the node map never\n * crosses the wire. This is that seed, as one pure rule both sides share.\n */\n\nconst CANVAS_ROOT_ID = '_@_'\n\n/** The back-pointer key a version of each parent kind carries. */\nexport const VERSION_PARENT_KEYS: Readonly<Record<string, string>> = {\n screen: 'screenId',\n layout: 'layoutId',\n component: 'componentId',\n form: 'formId',\n}\n\n/** A canvas with nothing on it but its root. */\nexport function rootOnlyCanvas(): Record<string, unknown> {\n return { [CANVAS_ROOT_ID]: { $id: CANVAS_ROOT_ID, componentId: 'div', nodes: [] } }\n}\n\n/**\n * The first version's document for [parentId] of [kind], from the parent's\n * stored fields with its node map already DECODED. Null for a kind with no\n * versions.\n */\nexport function firstVersionSeed(\n kind: string,\n parentId: string,\n hostId: string,\n parent: {\n nodes?: Record<string, unknown> | null\n rootId?: unknown\n props?: unknown\n },\n): Record<string, unknown> | null {\n const backPointer = VERSION_PARENT_KEYS[kind]\n if (!backPointer) return null\n const nodes = parent.nodes && Object.keys(parent.nodes).length ? parent.nodes : rootOnlyCanvas()\n return {\n [backPointer]: parentId,\n hostId,\n displayName: 'Initial version',\n nodes,\n ...(typeof parent.rootId === 'string' && parent.rootId ? { rootId: parent.rootId } : {}),\n ...(Array.isArray(parent.props) && parent.props.length ? { props: parent.props } : {}),\n }\n}\n"],"names":["CANVAS_ROOT_ID","VERSION_PARENT_KEYS","screen","layout","component","form","rootOnlyCanvas","$id","componentId","nodes","firstVersionSeed","kind","parentId","hostId","parent","backPointer","Object","keys","length","displayName","rootId","Array","isArray","props"],"mappings":";AAAA;;;;CAIC,GAED;;;;;;;;;;;CAWC,GAED,MAAMA,iBAAiB;AAEvB,gEAAgE,GAChE,OAAO,MAAMC,sBAAwD;IACnEC,QAAQ;IACRC,QAAQ;IACRC,WAAW;IACXC,MAAM;AACR,EAAC;AAED,8CAA8C,GAC9C,OAAO,SAASC;IACd,OAAO;QAAE,CAACN,eAAe,EAAE;YAAEO,KAAKP;YAAgBQ,aAAa;YAAOC,OAAO,EAAE;QAAC;IAAE;AACpF;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBACdC,IAAY,EACZC,QAAgB,EAChBC,MAAc,EACdC,MAIC;IAED,MAAMC,cAAcd,mBAAmB,CAACU,KAAK;IAC7C,IAAI,CAACI,aAAa,OAAO;IACzB,MAAMN,QAAQK,OAAOL,KAAK,IAAIO,OAAOC,IAAI,CAACH,OAAOL,KAAK,EAAES,MAAM,GAAGJ,OAAOL,KAAK,GAAGH;IAChF,OAAO;QACL,CAACS,YAAY,EAAEH;QACfC;QACAM,aAAa;QACbV;OACI,OAAOK,OAAOM,MAAM,KAAK,YAAYN,OAAOM,MAAM,GAAG;QAAEA,QAAQN,OAAOM,MAAM;IAAC,IAAI,CAAC,GAClFC,MAAMC,OAAO,CAACR,OAAOS,KAAK,KAAKT,OAAOS,KAAK,CAACL,MAAM,GAAG;QAAEK,OAAOT,OAAOS,KAAK;IAAC,IAAI,CAAC;AAExF"}
|
|
@@ -105,6 +105,14 @@ export declare const LOCAL_BUSINESS_TYPES: readonly [{
|
|
|
105
105
|
readonly label: "Animal shelter";
|
|
106
106
|
}];
|
|
107
107
|
export type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value'];
|
|
108
|
+
/**
|
|
109
|
+
* {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read
|
|
110
|
+
* a literal tuple: the native apps' contracts (AGL-3668).
|
|
111
|
+
*/
|
|
112
|
+
export declare const LOCAL_BUSINESS_TYPE_OPTIONS: ReadonlyArray<{
|
|
113
|
+
value: string;
|
|
114
|
+
label: string;
|
|
115
|
+
}>;
|
|
108
116
|
/** At most this many areas are published; a service area is a list, not a gazetteer. */
|
|
109
117
|
export declare const AREA_SERVED_MAX = 20;
|
|
110
118
|
export declare const AREA_SERVED_NAME_MAX_LENGTH = 120;
|
|
@@ -126,6 +126,10 @@
|
|
|
126
126
|
label: 'Animal shelter'
|
|
127
127
|
}
|
|
128
128
|
];
|
|
129
|
+
/**
|
|
130
|
+
* {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read
|
|
131
|
+
* a literal tuple: the native apps' contracts (AGL-3668).
|
|
132
|
+
*/ export const LOCAL_BUSINESS_TYPE_OPTIONS = LOCAL_BUSINESS_TYPES;
|
|
129
133
|
/** At most this many areas are published; a service area is a list, not a gazetteer. */ export const AREA_SERVED_MAX = 20;
|
|
130
134
|
export const AREA_SERVED_NAME_MAX_LENGTH = 120;
|
|
131
135
|
/** Seven days, twice over — room for split shifts without inviting a timetable. */ export const OPENING_HOURS_MAX_ROWS = 14;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/local-business.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A site entity that is a LOCAL BUSINESS (AGL-3383).\n *\n * `host.seo.entity.type` answers Organization or Person and keeps answering\n * it: it is a numeric enum stored as `\"1\"`/`\"2\"`, read by the author editor,\n * the logo card and the AI audit, and a third value there would be a third\n * value each of them has to learn. A plumber is still an Organization — the\n * business type REFINES that answer rather than replacing it — so it is a\n * field of its own, `businessType`, holding the `schema.org` subtype name.\n *\n * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the\n * console's Select is built from too. A stored value outside it (a typo, a\n * type removed from the list) publishes the plain Organization it would have\n * published before the field existed, rather than an `@type` no consumer\n * recognizes.\n */\n\n/** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */\nexport const LOCAL_BUSINESS_TYPES = [\n { value: 'LocalBusiness', label: 'Local business (general)' },\n { value: 'HomeAndConstructionBusiness', label: 'Home and construction' },\n { value: 'GeneralContractor', label: 'General contractor' },\n { value: 'HousePainter', label: 'House painter' },\n { value: 'Plumber', label: 'Plumber' },\n { value: 'Electrician', label: 'Electrician' },\n { value: 'HVACBusiness', label: 'Heating and air (HVAC)' },\n { value: 'Locksmith', label: 'Locksmith' },\n { value: 'RoofingContractor', label: 'Roofing contractor' },\n { value: 'MovingCompany', label: 'Moving company' },\n { value: 'ProfessionalService', label: 'Professional service' },\n { value: 'Store', label: 'Store' },\n { value: 'FoodEstablishment', label: 'Food and drink' },\n { value: 'Restaurant', label: 'Restaurant' },\n { value: 'HealthAndBeautyBusiness', label: 'Health and beauty' },\n { value: 'MedicalBusiness', label: 'Medical practice' },\n { value: 'Dentist', label: 'Dentist' },\n { value: 'AutomotiveBusiness', label: 'Automotive' },\n { value: 'LegalService', label: 'Legal service' },\n { value: 'AccountingService', label: 'Accounting' },\n { value: 'RealEstateAgent', label: 'Real estate agent' },\n { value: 'SportsActivityLocation', label: 'Gym, sports or fitness' },\n { value: 'ChildCare', label: 'Child care' },\n { value: 'AnimalShelter', label: 'Animal shelter' },\n] as const\n\nexport type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value']\n\n/** At most this many areas are published; a service area is a list, not a gazetteer. */\nexport const AREA_SERVED_MAX = 20\nexport const AREA_SERVED_NAME_MAX_LENGTH = 120\n/** Seven days, twice over — room for split shifts without inviting a timetable. */\nexport const OPENING_HOURS_MAX_ROWS = 14\n/** Google reads a price range past 100 characters as not being one. */\nexport const PRICE_RANGE_MAX_LENGTH = 100\nexport const PAYMENT_ACCEPTED_MAX_LENGTH = 200\n\n/**\n * The stored business type, as the `schema.org` name it publishes under, or\n * `undefined` when there is none or it is not on the list.\n *\n * Case-insensitive, returning the canonical spelling, because `@type` is\n * case-sensitive to every consumer and `plumber` would name nothing.\n */\nexport function localBusinessType(value: unknown): LocalBusinessType | undefined {\n if (typeof value !== 'string') return undefined\n const wanted = value.trim().toLowerCase()\n if (!wanted) return undefined\n return LOCAL_BUSINESS_TYPES.find((entry) => entry.value.toLowerCase() === wanted)\n ?.value\n}\n\n/**\n * The areas a business serves: trimmed, blanks dropped, de-duplicated without\n * regard to case, and capped.\n *\n * Accepts the stored array or a newline-separated string, so a value typed\n * into the console's text box reads the same whichever shape reached the\n * document.\n */\nexport function normalizeAreaServed(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(/\\r?\\n/)\n : []\n const seen = new Set<string>()\n const areas: string[] = []\n for (const item of items) {\n if (typeof item !== 'string') continue\n const name = item.trim().replace(/\\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n areas.push(name)\n if (areas.length >= AREA_SERVED_MAX) break\n }\n return areas\n}\n\nconst DAYS = [\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n 'Sunday',\n] as const\n\nexport type OpeningHoursDay = (typeof DAYS)[number]\n\n/** One line of opening hours: these days, from `opens` to `closes`. */\nexport interface OpeningHoursRow {\n days: OpeningHoursDay[]\n /** `HH:MM`, 24-hour. */\n opens: string\n /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */\n closes: string\n}\n\n/**\n * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and\n * `Monday` all work, which is every way a person abbreviates a weekday. A\n * single letter is refused: `T` and `S` are each two days.\n */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return DAYS.findIndex((day) => day.toLowerCase().startsWith(wanted))\n}\n\n/** `9:00` → `09:00`; anything that is not a real time of day → `''`. */\nconst clockTime = (hours: string, minutes: string): string => {\n const h = Number(hours)\n const m = Number(minutes)\n if (h > 23 || m > 59) return ''\n return `${String(h).padStart(2, '0')}:${minutes}`\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** One line, or `null` when any part of it does not read. */\nfunction parseOpeningHoursLine(line: string): OpeningHoursRow | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const opens = clockTime(match[2], match[3])\n const closes = clockTime(match[4], match[5])\n if (!opens || !closes) return null\n const days: OpeningHoursDay[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n for (let step = 0; step <= (to - from + 7) % 7; step++) {\n const day = DAYS[(from + step) % 7]\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, opens, closes } : null\n}\n\nconst hoursLines = (value: unknown): string[] =>\n typeof value === 'string' ? value.split(/\\r?\\n/) : []\n\n/**\n * Opening hours, one line per set of days, in the syntax `schema.org`'s own\n * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,\n * `Mo,We,Fr 08:00-12:00`.\n *\n * A text box rather than a repeating group of pickers because it is what the\n * settings forms can hold without a new control, and because it is the form\n * a business already writes its hours in. A line that does not read — a\n * mistyped day, `25:00` — is DROPPED rather than guessed at: a published\n * opening time is a promise a customer drives across town on.\n */\nexport function parseOpeningHours(value: unknown): OpeningHoursRow[] {\n const rows: OpeningHoursRow[] = []\n for (const line of hoursLines(value)) {\n if (!line.trim()) continue\n const row = parseOpeningHoursLine(line)\n if (row) rows.push(row)\n if (rows.length >= OPENING_HOURS_MAX_ROWS) break\n }\n return rows\n}\n\n/**\n * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so\n * the console can say which one before it is saved rather than after it\n * silently fails to publish. Blank lines are not problems.\n */\nexport function invalidOpeningHoursLines(value: unknown): number[] {\n const invalid: number[] = []\n hoursLines(value).forEach((line, index) => {\n if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1)\n })\n return invalid\n}\n"],"names":["LOCAL_BUSINESS_TYPES","value","label","AREA_SERVED_MAX","AREA_SERVED_NAME_MAX_LENGTH","OPENING_HOURS_MAX_ROWS","PRICE_RANGE_MAX_LENGTH","PAYMENT_ACCEPTED_MAX_LENGTH","localBusinessType","undefined","wanted","trim","toLowerCase","find","entry","normalizeAreaServed","items","Array","isArray","split","seen","Set","areas","item","name","replace","slice","key","has","add","push","length","DAYS","dayIndex","token","findIndex","day","startsWith","clockTime","hours","minutes","h","Number","m","String","padStart","DASH","LINE","parseOpeningHoursLine","line","match","exec","opens","closes","days","part","ends","from","to","step","includes","hoursLines","parseOpeningHours","rows","row","invalidOpeningHoursLines","invalid","forEach","index"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAED,2FAA2F,GAC3F,OAAO,MAAMA,uBAAuB;IAClC;QAAEC,OAAO;QAAiBC,OAAO;IAA2B;IAC5D;QAAED,OAAO;QAA+BC,OAAO;IAAwB;IACvE;QAAED,OAAO;QAAqBC,OAAO;IAAqB;IAC1D;QAAED,OAAO;QAAgBC,OAAO;IAAgB;IAChD;QAAED,OAAO;QAAWC,OAAO;IAAU;IACrC;QAAED,OAAO;QAAeC,OAAO;IAAc;IAC7C;QAAED,OAAO;QAAgBC,OAAO;IAAyB;IACzD;QAAED,OAAO;QAAaC,OAAO;IAAY;IACzC;QAAED,OAAO;QAAqBC,OAAO;IAAqB;IAC1D;QAAED,OAAO;QAAiBC,OAAO;IAAiB;IAClD;QAAED,OAAO;QAAuBC,OAAO;IAAuB;IAC9D;QAAED,OAAO;QAASC,OAAO;IAAQ;IACjC;QAAED,OAAO;QAAqBC,OAAO;IAAiB;IACtD;QAAED,OAAO;QAAcC,OAAO;IAAa;IAC3C;QAAED,OAAO;QAA2BC,OAAO;IAAoB;IAC/D;QAAED,OAAO;QAAmBC,OAAO;IAAmB;IACtD;QAAED,OAAO;QAAWC,OAAO;IAAU;IACrC;QAAED,OAAO;QAAsBC,OAAO;IAAa;IACnD;QAAED,OAAO;QAAgBC,OAAO;IAAgB;IAChD;QAAED,OAAO;QAAqBC,OAAO;IAAa;IAClD;QAAED,OAAO;QAAmBC,OAAO;IAAoB;IACvD;QAAED,OAAO;QAA0BC,OAAO;IAAyB;IACnE;QAAED,OAAO;QAAaC,OAAO;IAAa;IAC1C;QAAED,OAAO;QAAiBC,OAAO;IAAiB;CACnD,CAAS;AAIV,sFAAsF,GACtF,OAAO,MAAMC,kBAAkB,GAAE;AACjC,OAAO,MAAMC,8BAA8B,IAAG;AAC9C,iFAAiF,GACjF,OAAO,MAAMC,yBAAyB,GAAE;AACxC,qEAAqE,GACrE,OAAO,MAAMC,yBAAyB,IAAG;AACzC,OAAO,MAAMC,8BAA8B,IAAG;AAE9C;;;;;;CAMC,GACD,OAAO,SAASC,kBAAkBP,KAAc;QAIvCD;IAHP,IAAI,OAAOC,UAAU,UAAU,OAAOQ;IACtC,MAAMC,SAAST,MAAMU,IAAI,GAAGC,WAAW;IACvC,IAAI,CAACF,QAAQ,OAAOD;IACpB,QAAOT,6BAAAA,qBAAqBa,IAAI,CAAC,CAACC,QAAUA,MAAMb,KAAK,CAACW,WAAW,OAAOF,4BAAnEV,2BACHC,KAAK;AACX;AAEA;;;;;;;CAOC,GACD,OAAO,SAASc,oBAAoBd,KAAc;IAChD,MAAMe,QAAQC,MAAMC,OAAO,CAACjB,SACxBA,QACA,OAAOA,UAAU,WACfA,MAAMkB,KAAK,CAAC,WACZ,EAAE;IACR,MAAMC,OAAO,IAAIC;IACjB,MAAMC,QAAkB,EAAE;IAC1B,KAAK,MAAMC,QAAQP,MAAO;QACxB,IAAI,OAAOO,SAAS,UAAU;QAC9B,MAAMC,OAAOD,KAAKZ,IAAI,GAAGc,OAAO,CAAC,QAAQ,KAAKC,KAAK,CAAC,GAAGtB;QACvD,MAAMuB,MAAMH,KAAKZ,WAAW;QAC5B,IAAI,CAACY,QAAQJ,KAAKQ,GAAG,CAACD,MAAM;QAC5BP,KAAKS,GAAG,CAACF;QACTL,MAAMQ,IAAI,CAACN;QACX,IAAIF,MAAMS,MAAM,IAAI5B,iBAAiB;IACvC;IACA,OAAOmB;AACT;AAEA,MAAMU,OAAO;IACX;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAaD;;;;CAIC,GACD,MAAMC,WAAW,CAACC;IAChB,MAAMxB,SAASwB,MAAMvB,IAAI,GAAGC,WAAW;IACvC,IAAIF,OAAOqB,MAAM,GAAG,GAAG,OAAO,CAAC;IAC/B,OAAOC,KAAKG,SAAS,CAAC,CAACC,MAAQA,IAAIxB,WAAW,GAAGyB,UAAU,CAAC3B;AAC9D;AAEA,sEAAsE,GACtE,MAAM4B,YAAY,CAACC,OAAeC;IAChC,MAAMC,IAAIC,OAAOH;IACjB,MAAMI,IAAID,OAAOF;IACjB,IAAIC,IAAI,MAAME,IAAI,IAAI,OAAO;IAC7B,OAAO,GAAGC,OAAOH,GAAGI,QAAQ,CAAC,GAAG,KAAK,CAAC,EAAEL,SAAS;AACnD;AAEA,MAAMM,OAAO;AACb,MAAMC,OAAO;AAEb,2DAA2D,GAC3D,SAASC,sBAAsBC,IAAY;IACzC,MAAMC,QAAQH,KAAKI,IAAI,CAACF,KAAKtC,IAAI;IACjC,IAAI,CAACuC,OAAO,OAAO;IACnB,MAAME,QAAQd,UAAUY,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE;IAC1C,MAAMG,SAASf,UAAUY,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE;IAC3C,IAAI,CAACE,SAAS,CAACC,QAAQ,OAAO;IAC9B,MAAMC,OAA0B,EAAE;IAClC,KAAK,MAAMC,QAAQL,KAAK,CAAC,EAAE,CAAC/B,KAAK,CAAC,KAAM;QACtC,MAAMqC,OAAOD,KAAK5C,IAAI,GAAGQ,KAAK,CAAC2B;QAC/B,IAAIU,KAAKzB,MAAM,GAAG,GAAG,OAAO;QAC5B,MAAM0B,OAAOxB,SAASuB,IAAI,CAAC,EAAE;QAC7B,MAAME,KAAKzB,SAASuB,IAAI,CAACA,KAAKzB,MAAM,GAAG,EAAE;QACzC,IAAI0B,OAAO,KAAKC,KAAK,GAAG,OAAO;QAC/B,kEAAkE;QAClE,IAAK,IAAIC,OAAO,GAAGA,QAAQ,AAACD,CAAAA,KAAKD,OAAO,CAAA,IAAK,GAAGE,OAAQ;YACtD,MAAMvB,MAAMJ,IAAI,CAAC,AAACyB,CAAAA,OAAOE,IAAG,IAAK,EAAE;YACnC,IAAI,CAACL,KAAKM,QAAQ,CAACxB,MAAMkB,KAAKxB,IAAI,CAACM;QACrC;IACF;IACA,OAAOkB,KAAKvB,MAAM,GAAG;QAAEuB;QAAMF;QAAOC;IAAO,IAAI;AACjD;AAEA,MAAMQ,aAAa,CAAC5D,QAClB,OAAOA,UAAU,WAAWA,MAAMkB,KAAK,CAAC,WAAW,EAAE;AAEvD;;;;;;;;;;CAUC,GACD,OAAO,SAAS2C,kBAAkB7D,KAAc;IAC9C,MAAM8D,OAA0B,EAAE;IAClC,KAAK,MAAMd,QAAQY,WAAW5D,OAAQ;QACpC,IAAI,CAACgD,KAAKtC,IAAI,IAAI;QAClB,MAAMqD,MAAMhB,sBAAsBC;QAClC,IAAIe,KAAKD,KAAKjC,IAAI,CAACkC;QACnB,IAAID,KAAKhC,MAAM,IAAI1B,wBAAwB;IAC7C;IACA,OAAO0D;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,yBAAyBhE,KAAc;IACrD,MAAMiE,UAAoB,EAAE;IAC5BL,WAAW5D,OAAOkE,OAAO,CAAC,CAAClB,MAAMmB;QAC/B,IAAInB,KAAKtC,IAAI,MAAM,CAACqC,sBAAsBC,OAAOiB,QAAQpC,IAAI,CAACsC,QAAQ;IACxE;IACA,OAAOF;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/local-business.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A site entity that is a LOCAL BUSINESS (AGL-3383).\n *\n * `host.seo.entity.type` answers Organization or Person and keeps answering\n * it: it is a numeric enum stored as `\"1\"`/`\"2\"`, read by the author editor,\n * the logo card and the AI audit, and a third value there would be a third\n * value each of them has to learn. A plumber is still an Organization — the\n * business type REFINES that answer rather than replacing it — so it is a\n * field of its own, `businessType`, holding the `schema.org` subtype name.\n *\n * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the\n * console's Select is built from too. A stored value outside it (a typo, a\n * type removed from the list) publishes the plain Organization it would have\n * published before the field existed, rather than an `@type` no consumer\n * recognizes.\n */\n\n/** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */\nexport const LOCAL_BUSINESS_TYPES = [\n { value: 'LocalBusiness', label: 'Local business (general)' },\n { value: 'HomeAndConstructionBusiness', label: 'Home and construction' },\n { value: 'GeneralContractor', label: 'General contractor' },\n { value: 'HousePainter', label: 'House painter' },\n { value: 'Plumber', label: 'Plumber' },\n { value: 'Electrician', label: 'Electrician' },\n { value: 'HVACBusiness', label: 'Heating and air (HVAC)' },\n { value: 'Locksmith', label: 'Locksmith' },\n { value: 'RoofingContractor', label: 'Roofing contractor' },\n { value: 'MovingCompany', label: 'Moving company' },\n { value: 'ProfessionalService', label: 'Professional service' },\n { value: 'Store', label: 'Store' },\n { value: 'FoodEstablishment', label: 'Food and drink' },\n { value: 'Restaurant', label: 'Restaurant' },\n { value: 'HealthAndBeautyBusiness', label: 'Health and beauty' },\n { value: 'MedicalBusiness', label: 'Medical practice' },\n { value: 'Dentist', label: 'Dentist' },\n { value: 'AutomotiveBusiness', label: 'Automotive' },\n { value: 'LegalService', label: 'Legal service' },\n { value: 'AccountingService', label: 'Accounting' },\n { value: 'RealEstateAgent', label: 'Real estate agent' },\n { value: 'SportsActivityLocation', label: 'Gym, sports or fitness' },\n { value: 'ChildCare', label: 'Child care' },\n { value: 'AnimalShelter', label: 'Animal shelter' },\n] as const\n\nexport type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value']\n\n/**\n * {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read\n * a literal tuple: the native apps' contracts (AGL-3668).\n */\nexport const LOCAL_BUSINESS_TYPE_OPTIONS: ReadonlyArray<{ value: string; label: string }> =\n LOCAL_BUSINESS_TYPES\n\n/** At most this many areas are published; a service area is a list, not a gazetteer. */\nexport const AREA_SERVED_MAX = 20\nexport const AREA_SERVED_NAME_MAX_LENGTH = 120\n/** Seven days, twice over — room for split shifts without inviting a timetable. */\nexport const OPENING_HOURS_MAX_ROWS = 14\n/** Google reads a price range past 100 characters as not being one. */\nexport const PRICE_RANGE_MAX_LENGTH = 100\nexport const PAYMENT_ACCEPTED_MAX_LENGTH = 200\n\n/**\n * The stored business type, as the `schema.org` name it publishes under, or\n * `undefined` when there is none or it is not on the list.\n *\n * Case-insensitive, returning the canonical spelling, because `@type` is\n * case-sensitive to every consumer and `plumber` would name nothing.\n */\nexport function localBusinessType(value: unknown): LocalBusinessType | undefined {\n if (typeof value !== 'string') return undefined\n const wanted = value.trim().toLowerCase()\n if (!wanted) return undefined\n return LOCAL_BUSINESS_TYPES.find((entry) => entry.value.toLowerCase() === wanted)\n ?.value\n}\n\n/**\n * The areas a business serves: trimmed, blanks dropped, de-duplicated without\n * regard to case, and capped.\n *\n * Accepts the stored array or a newline-separated string, so a value typed\n * into the console's text box reads the same whichever shape reached the\n * document.\n */\nexport function normalizeAreaServed(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(/\\r?\\n/)\n : []\n const seen = new Set<string>()\n const areas: string[] = []\n for (const item of items) {\n if (typeof item !== 'string') continue\n const name = item.trim().replace(/\\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n areas.push(name)\n if (areas.length >= AREA_SERVED_MAX) break\n }\n return areas\n}\n\nconst DAYS = [\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n 'Sunday',\n] as const\n\nexport type OpeningHoursDay = (typeof DAYS)[number]\n\n/** One line of opening hours: these days, from `opens` to `closes`. */\nexport interface OpeningHoursRow {\n days: OpeningHoursDay[]\n /** `HH:MM`, 24-hour. */\n opens: string\n /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */\n closes: string\n}\n\n/**\n * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and\n * `Monday` all work, which is every way a person abbreviates a weekday. A\n * single letter is refused: `T` and `S` are each two days.\n */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return DAYS.findIndex((day) => day.toLowerCase().startsWith(wanted))\n}\n\n/** `9:00` → `09:00`; anything that is not a real time of day → `''`. */\nconst clockTime = (hours: string, minutes: string): string => {\n const h = Number(hours)\n const m = Number(minutes)\n if (h > 23 || m > 59) return ''\n return `${String(h).padStart(2, '0')}:${minutes}`\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** One line, or `null` when any part of it does not read. */\nfunction parseOpeningHoursLine(line: string): OpeningHoursRow | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const opens = clockTime(match[2], match[3])\n const closes = clockTime(match[4], match[5])\n if (!opens || !closes) return null\n const days: OpeningHoursDay[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n for (let step = 0; step <= (to - from + 7) % 7; step++) {\n const day = DAYS[(from + step) % 7]\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, opens, closes } : null\n}\n\nconst hoursLines = (value: unknown): string[] =>\n typeof value === 'string' ? value.split(/\\r?\\n/) : []\n\n/**\n * Opening hours, one line per set of days, in the syntax `schema.org`'s own\n * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,\n * `Mo,We,Fr 08:00-12:00`.\n *\n * A text box rather than a repeating group of pickers because it is what the\n * settings forms can hold without a new control, and because it is the form\n * a business already writes its hours in. A line that does not read — a\n * mistyped day, `25:00` — is DROPPED rather than guessed at: a published\n * opening time is a promise a customer drives across town on.\n */\nexport function parseOpeningHours(value: unknown): OpeningHoursRow[] {\n const rows: OpeningHoursRow[] = []\n for (const line of hoursLines(value)) {\n if (!line.trim()) continue\n const row = parseOpeningHoursLine(line)\n if (row) rows.push(row)\n if (rows.length >= OPENING_HOURS_MAX_ROWS) break\n }\n return rows\n}\n\n/**\n * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so\n * the console can say which one before it is saved rather than after it\n * silently fails to publish. Blank lines are not problems.\n */\nexport function invalidOpeningHoursLines(value: unknown): number[] {\n const invalid: number[] = []\n hoursLines(value).forEach((line, index) => {\n if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1)\n })\n return invalid\n}\n"],"names":["LOCAL_BUSINESS_TYPES","value","label","LOCAL_BUSINESS_TYPE_OPTIONS","AREA_SERVED_MAX","AREA_SERVED_NAME_MAX_LENGTH","OPENING_HOURS_MAX_ROWS","PRICE_RANGE_MAX_LENGTH","PAYMENT_ACCEPTED_MAX_LENGTH","localBusinessType","undefined","wanted","trim","toLowerCase","find","entry","normalizeAreaServed","items","Array","isArray","split","seen","Set","areas","item","name","replace","slice","key","has","add","push","length","DAYS","dayIndex","token","findIndex","day","startsWith","clockTime","hours","minutes","h","Number","m","String","padStart","DASH","LINE","parseOpeningHoursLine","line","match","exec","opens","closes","days","part","ends","from","to","step","includes","hoursLines","parseOpeningHours","rows","row","invalidOpeningHoursLines","invalid","forEach","index"],"mappings":"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"}
|
|
@@ -193,6 +193,9 @@ import { SAFE_HREF_PATTERN, resolveScreenHref, splitLinkValue } from "./screen-l
|
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
push(level > 0 ? `${'#'.repeat(level)} ${text}` : text);
|
|
196
|
+
// A Typography renders the elements it holds after its text
|
|
197
|
+
// (AGL-3672), so they read after it here too.
|
|
198
|
+
recurse();
|
|
196
199
|
return;
|
|
197
200
|
}
|
|
198
201
|
case 'markdown':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/page-markdown.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A published page as Markdown — the representation an agent gets when it\n * sends `Accept: text/markdown` (acceptmarkdown.com).\n *\n * ## Why serialize the NODE TREE and not the rendered HTML\n *\n * The obvious implementation is to render the page and run an HTML-to-Markdown\n * converter over it. Every site on this platform is a node document, and the\n * tree carries what the HTML has already thrown away: which text is a heading\n * because the author said so rather than because it is large, which container\n * is the page's content region, and which subtree is navigation. Converting\n * the HTML back would be reconstructing, by heuristic, information we are\n * holding.\n *\n * It is also cheaper by a wide margin — no React render, no plugin registry,\n * no MUI — which is what makes this affordable to serve from a cached route on\n * every site rather than only where somebody enabled it.\n *\n * ## The content region, not the document\n *\n * Serialization starts at the document's `main` landmark, which\n * `stampDocumentLandmark` has already placed on the layout slot (the page\n * content between the chrome) or on the screen root. That is what makes the\n * output nav-free and footer-free without this module knowing anything about\n * how a particular site builds its header — the same property that makes a\n * Markdown variant worth serving at all.\n *\n * A page with no landmark falls back to the whole tree. Some content is always\n * better than an empty document, and a page that shipped no landmark\n * deliberately (see `document-landmark.ts`) has no content region to prefer.\n *\n * ## Chrome that lives INSIDE the content region\n *\n * A handful of components are navigation wherever they sit — an app bar, a nav\n * menu, a language switcher, a pagination strip. They are skipped by id, and\n * the list is deliberately short: everything not named recurses, so a plugin\n * block nobody here has heard of still contributes its text instead of\n * vanishing. Silence is the one failure mode that would be invisible to the\n * author.\n */\n\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport { authorHtmlToMarkdown, inlineAuthorHtmlToMarkdown } from './author-html-markdown'\nimport { resolveMarkdownSourceLinks } from './markdown-lite'\nimport { absoluteMediaSrc } from './media-ref'\nimport {\n SAFE_HREF_PATTERN,\n resolveScreenHref,\n splitLinkValue,\n} from './screen-link-value'\n\n/** The node shape this serializer reads; keeps callers free of the full type. */\nexport interface PageMarkdownNode {\n componentId?: string\n props?: Record<string, unknown> | null\n /** Normalized documents carry child IDS; this never reads a denormalized tree. */\n nodes?: unknown\n}\n\n/** A flat node map keyed by node id — exactly what the page route already holds. */\nexport type PageMarkdownNodes = Record<string, PageMarkdownNode>\n\n/** The element name a node was told to render as (`props.component`). */\nconst elementOf = (node: PageMarkdownNode | undefined): string =>\n String(node?.props?.['component'] ?? '').toLowerCase()\n\n/** Components that are navigation wherever they appear. See the module note. */\nconst CHROME_COMPONENTS: ReadonlySet<string> = new Set([\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiBreadcrumbs',\n 'navMenu',\n 'searchBox',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'muiPagination',\n // An icon is decoration with no accessible text of its own. Its LABEL, where\n // one exists, belongs to the control around it and is emitted there.\n 'icon',\n])\n\n/**\n * Whether a node is navigation rather than content: a chrome component, or\n * an element its author declared `nav`, `header` or `footer`. Exported so a\n * reader of a page's CONTENT — the SEO audit's heading and image scan\n * (AGL-2910) — skips exactly what this serializer skips.\n */\nexport function isPageChromeNode(node: PageMarkdownNode | undefined): boolean {\n if (!node) return false\n if (CHROME_COMPONENTS.has(String(node.componentId ?? ''))) return true\n const element = elementOf(node)\n return element === 'nav' || element === 'header' || element === 'footer'\n}\n\n/** Heading levels, as either `props.component` or `props.variant` spells them. */\nconst HEADING = /^h([1-6])$/\n\n/**\n * The heading level a text node publishes, or 0 for body copy.\n *\n * `component` wins over `variant` because they answer different questions and\n * the platform lets them disagree: `variant` is the type STYLE (what size it is\n * drawn at) and `component` is the ELEMENT (what it means). An author who set a\n * hero line to look like an h1 but render as a `p` said it is not a heading,\n * and the Markdown has to agree with the HTML or the two documents describe\n * different pages.\n */\nexport function headingLevelOf(props: Record<string, unknown> | null | undefined): number {\n const element = String(props?.['component'] ?? '').toLowerCase()\n if (element) {\n const asElement = HEADING.exec(element)\n return asElement ? Number(asElement[1]) : 0\n }\n const variant = String(props?.['variant'] ?? '').toLowerCase()\n const asVariant = HEADING.exec(variant)\n return asVariant ? Number(asVariant[1]) : 0\n}\n\n/**\n * Escape the Markdown control characters that would otherwise re-parse.\n *\n * Only the characters that START a construct at the position they can occur in:\n * a leading `#`, `>`, `-`, `+` or `1.` on its own line, and the emphasis,\n * backtick and bracket characters anywhere. Escaping more than that produces\n * output littered with backslashes, which is worse to read and worse to embed\n * than the rare false heading it prevents.\n */\nexport function escapeMarkdownText(value: string): string {\n return value\n .replace(/([\\\\`*_[\\]<>])/g, '\\\\$1')\n .replace(/^(\\s*)([#>])/gm, '$1\\\\$2')\n .replace(/^(\\s*)([-+])(\\s)/gm, '$1\\\\$2$3')\n .replace(/^(\\s*)(\\d+)\\.(\\s)/gm, '$1$2\\\\.$3')\n}\n\n/** Collapse runs of whitespace to single spaces, as inline text renders. */\nconst collapse = (value: string): string => value.replace(/\\s+/g, ' ').trim()\n\n/** Resolution context for values that are stored as references, not URLs. */\nexport interface PageMarkdownContext {\n /** Site origin, for absolutizing media and site-relative links. */\n origin?: string | null\n /** Host id, so a restricted org asset resolves for the site rendering it. */\n hostId?: string\n /**\n * The routing map screen links resolve against (`screenRoutes`), and the\n * link references in authored markdown with them (AGL-3118).\n */\n screenRoutes?: Record<string, string> | null\n}\n\n/**\n * A link target as Markdown should carry it: absolute where we can make it so.\n *\n * Site-relative is fine in a page — the browser has a document to resolve\n * against. A Markdown body has no such context by the time an agent has copied\n * it into a prompt, so a relative href there is a link to nowhere.\n */\nfunction absoluteHref(href: string, context?: PageMarkdownContext): string {\n const value = href.trim()\n if (!value) return ''\n if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith('//')) return value\n const origin = context?.origin?.replace(/\\/+$/, '') ?? ''\n if (!origin) return value\n return value.startsWith('/') ? `${origin}${value}` : `${origin}/${value}`\n}\n\n/** The text a control shows, preferring its rich label over the plain one. */\nfunction labelOf(\n props: Record<string, unknown> | null | undefined,\n context?: PageMarkdownContext,\n): string {\n const html = props?.['html']\n if (typeof html === 'string' && html.trim()) {\n const rich = inlineAuthorHtmlToMarkdown(html, context)\n if (rich.trim()) return collapse(rich)\n }\n const children = props?.['children']\n return typeof children === 'string' ? collapse(escapeMarkdownText(children)) : ''\n}\n\n/** A block emitted by the walk, before the blocks are joined. */\ninterface Block {\n /** Markdown text of the block, already escaped. */\n text: string\n}\n\n/**\n * Serialize the subtree at `rootId` into Markdown blocks.\n *\n * A depth budget and a visited set, because a node map is data: a document\n * restored from a backup, or written through the REST API, can name a child\n * that names it back. Recursing that tree is a stack overflow inside a request\n * handler, i.e. a 500 on a page that renders fine — the renderer tolerates it\n * because React unmounts a cycle, and this has nothing to stop it but itself.\n */\nfunction walk(\n nodes: PageMarkdownNodes,\n rootId: string,\n context: PageMarkdownContext | undefined,\n blocks: Block[],\n seen: Set<string>,\n depth: number,\n): void {\n if (depth > 64 || seen.has(rootId)) return\n const node = nodes[rootId]\n if (!node) return\n seen.add(rootId)\n\n const componentId = String(node.componentId ?? '')\n // Chrome components, and `nav`, `header` and `footer` by their OWN\n // declaration — an author who set the element picker to one of them named\n // it navigation, and that is a stronger signal than any component-id list\n // can be.\n if (isPageChromeNode(node)) return\n\n const props = node.props ?? {}\n const childIds = Array.isArray(node.nodes)\n ? node.nodes.filter((id): id is string => typeof id === 'string')\n : []\n const recurse = (): void => {\n for (const childId of childIds) {\n walk(nodes, childId, context, blocks, seen, depth + 1)\n }\n }\n const push = (text: string): void => {\n if (text.trim()) blocks.push({ text: text.trim() })\n }\n\n switch (componentId) {\n case 'muiTypography':\n case 'muiInlineText':\n case 'muiListItemText': {\n const level = headingLevelOf(props)\n const text = labelOf(props, context)\n if (!text) {\n // A Typography with no text of its own but children beneath it is a\n // wrapper, which is how rich composites are built. Recurse rather than\n // dropping the subtree.\n recurse()\n return\n }\n push(level > 0 ? `${'#'.repeat(level)} ${text}` : text)\n return\n }\n case 'markdown': {\n /*\n Already Markdown. Emitted VERBATIM and never re-escaped: `content` is\n the markdown-lite source an author typed, so escaping it would turn\n their headings into literal `#` characters — the one transformation\n that can only make this output worse. Its link REFERENCES are the one\n exception (AGL-3118), resolved as the page resolves them.\n */\n const content = props['content']\n if (typeof content === 'string') push(authoredMarkdown(content, context))\n return\n }\n case 'custom-html': {\n const html = props['html'] ?? props['children']\n if (typeof html === 'string') push(authorHtmlToMarkdown(html, context))\n return\n }\n case 'image': {\n const src = absoluteMediaSrc(String(props['src'] ?? '').trim(), {\n hostId: context?.hostId,\n origin: context?.origin,\n })\n if (!src) return\n const alt = collapse(String(props['alt'] ?? ''))\n push(``)\n return\n }\n case 'video':\n case 'videoEmbed': {\n const src = String(props['src'] ?? props['url'] ?? '').trim()\n if (!src) return\n push(`[Video](${absoluteHref(src, context)})`)\n return\n }\n case 'muiButton':\n case 'muiScreenLink': {\n const label = labelOf(props, context) || 'Link'\n const href = screenLinkHref(props, context)\n push(href ? `[${label}](${href})` : label)\n return\n }\n case 'muiAccordionSummary': {\n /*\n An accordion's question (AGL-3575). The summary carries its text in\n its own props, not in a child node, so the default recursion below\n emitted NOTHING for it — every FAQ on every site reached an agent as\n a run of orphaned answers. It is a heading because that is what the\n HTML publishes: MUI renders the summary inside its `h3` heading slot,\n and the Markdown has to agree with the page or the two describe\n different documents. A linked header (AGL-1232) keeps its link, as\n the page does.\n */\n const text = labelOf(props, context)\n if (!text) {\n recurse()\n return\n }\n const href =\n typeof props['screenId'] === 'string' && props['screenId']\n ? screenLinkHref(props, context)\n : ''\n push(href ? `### [${text}](${href})` : `### ${text}`)\n return\n }\n case 'muiList': {\n /*\n The items are gathered into ONE block so they join as a list. Walking\n them as siblings would put a blank line between every bullet, which is\n a sequence of paragraphs beginning with a dash rather than a list.\n */\n const items: Block[] = []\n for (const childId of childIds) {\n walk(nodes, childId, context, items, seen, depth + 1)\n }\n const lines = items\n .map((item) => item.text.replace(/\\n/g, '\\n '))\n .filter(Boolean)\n .map((text) => `- ${text}`)\n push(lines.join('\\n'))\n return\n }\n default:\n recurse()\n }\n}\n\n/**\n * Where a Button or Screen Link points.\n *\n * A screen link stores a SCREEN ID, not a path, and the path it resolves to is\n * a per-request question — `screenRoutes` is the routing map the router\n * actually honors, which is not the map publishing wrote (a collection's list\n * template answers at the collection's slug, template screens answer nowhere).\n * Emitting `href` alone would publish, in Markdown, the dead links AGL-1998\n * removed from the HTML.\n *\n * `splitLinkValue` + `resolveScreenHref` rather than a local reading of the\n * two props, because WHICH SLOT a screen reference arrives in is not fixed\n * (AGL-1335): a `Link`-typed component prop is bound to whichever of\n * `screenId`/`href` the author reached for, and a value in the `href` slot\n * carries the stored `screen:<id>` spelling. Reading `screenId` alone published\n * that token verbatim as a link target on every page built from a prop-fed\n * CTA — ten of them on aglyn.com, unfollowable by the agents `/llms.txt` sends\n * here (AGL-2740). These are the same two functions `useLinkTarget` resolves\n * the HTML with, so the two representations of a page cannot drift again.\n *\n * An authored screen id that the map has lost resolves to NO link, exactly as\n * the HTML renders it: falling back to the stored `href` there would re-publish\n * the dead path AGL-1998 removed, which is the failure this function's own\n * contract exists to prevent.\n */\nfunction screenLinkHref(\n props: Record<string, unknown>,\n context?: PageMarkdownContext,\n): string {\n const target = splitLinkValue(\n typeof props['screenId'] === 'string' ? props['screenId'] : undefined,\n typeof props['href'] === 'string' ? props['href'] : undefined,\n )\n if (target.screenId) {\n const path = resolveScreenHref(context?.screenRoutes ?? undefined, target.screenId)\n return path ? absoluteHref(path, context) : ''\n }\n // The same protocol guard the linking elements apply. A `javascript:` href\n // is inert in a Markdown body, but it is still a target an agent may be\n // asked to follow, and a representation that offers one the page refuses to\n // render is describing a different document.\n return target.href && SAFE_HREF_PATTERN.test(target.href)\n ? absoluteHref(target.href, context)\n : ''\n}\n\n/**\n * Authored markdown-lite — a Markdown element's content, an entry's body —\n * as this document carries it: verbatim, except that each link REFERENCE\n * (AGL-3118) points where the page's own link does, through the same routing\n * map and the same `resolveScreenHref` as {@link screenLinkHref}, made\n * absolute for the reason {@link absoluteHref} gives. A reference the map\n * does not have becomes the link's text, as the page renders it: the stored\n * `entry:…` is not a URL any reader can follow.\n */\nfunction authoredMarkdown(\n source: string,\n context?: PageMarkdownContext,\n): string {\n return resolveMarkdownSourceLinks(source, (reference) => {\n const path = resolveScreenHref(context?.screenRoutes ?? undefined, reference)\n return path ? absoluteHref(path, context) : undefined\n })\n}\n\n/**\n * The node the document's content begins at.\n *\n * The `main` landmark first — `stampDocumentLandmark` puts it on the layout\n * slot or the screen root — then the layout slot by component id, then the\n * document root. Each fallback is wider than the last, so a tree that is\n * missing the marker still serializes rather than returning nothing.\n */\nexport function pageContentRootId(\n nodes: PageMarkdownNodes | null | undefined,\n rootId: string = NODE_ROOT_ID,\n): string | null {\n if (!nodes) return null\n for (const id in nodes) {\n if (elementOf(nodes[id]) === 'main') return id\n }\n for (const id in nodes) {\n if (nodes[id]?.componentId === 'layoutSlot') return id\n }\n return nodes[rootId] ? rootId : null\n}\n\n/** What a Markdown page carries above its body. */\nexport interface PageMarkdownFrontMatter {\n /** The `# ` heading. The page's own title, never the site's. */\n title?: string\n /** The meta description, emitted as the summary blockquote. */\n description?: string\n /** Canonical absolute URL, emitted as the source line. */\n canonicalUrl?: string\n /** ISO-8601 publication date, for a content entry. */\n publishedAt?: string\n /** ISO-8601 last-modified date, for a content entry. */\n updatedAt?: string\n /** Byline, for a content entry. */\n author?: string\n}\n\n/**\n * The whole Markdown representation of a page.\n *\n * @param body - already-Markdown body to use INSTEAD of walking nodes; the\n * content-entry case, where the stored body is markdown-lite source and\n * round-tripping it through the node tree could only lose fidelity.\n */\nexport function buildPageMarkdown(options: {\n nodes?: PageMarkdownNodes | null\n rootId?: string\n body?: string | null\n front?: PageMarkdownFrontMatter\n context?: PageMarkdownContext\n}): string {\n const front = options.front ?? {}\n const parts: string[] = []\n\n const title = collapse(String(front.title ?? ''))\n if (title) parts.push(`# ${escapeMarkdownText(title)}`)\n\n const description = collapse(String(front.description ?? ''))\n if (description) parts.push(`> ${escapeMarkdownText(description)}`)\n\n /*\n The dateline is one line rather than YAML front matter. Front matter is\n unparsed text to every Markdown reader that has not opted into it, so it\n arrives in an agent's context as a block of `key: value` noise above the\n content; a sentence is read correctly by all of them.\n */\n const meta: string[] = []\n if (front.author) meta.push(`By ${collapse(front.author)}`)\n if (front.publishedAt) meta.push(`Published ${front.publishedAt}`)\n if (front.updatedAt && front.updatedAt !== front.publishedAt) {\n meta.push(`Updated ${front.updatedAt}`)\n }\n if (meta.length) parts.push(`_${meta.join(' · ')}_`)\n\n if (typeof options.body === 'string' && options.body.trim()) {\n parts.push(authoredMarkdown(options.body.trim(), options.context))\n } else if (options.nodes) {\n const contentRoot = pageContentRootId(options.nodes, options.rootId)\n if (contentRoot) {\n const blocks: Block[] = []\n walk(options.nodes, contentRoot, options.context, blocks, new Set(), 0)\n for (const block of blocks) parts.push(block.text)\n }\n }\n\n if (front.canonicalUrl) {\n // A source line, and the reason it is worth the two lines it costs: an\n // agent that has copied this body into a prompt has no URL bar, so without\n // it the document cannot be cited, re-fetched, or linked to by whatever\n // reads it next.\n parts.push('---', `Source: ${front.canonicalUrl}`)\n }\n\n /*\n Collapse the runs of blank lines a nested walk leaves behind, and end with\n exactly one newline. A Markdown body is compared byte-for-byte by caches\n and by the conformance checkers agents run, so \"tidy\" here is a contract\n rather than an aesthetic.\n */\n return `${parts.join('\\n\\n').replace(/\\n{3,}/g, '\\n\\n').trim()}\\n`\n}\n"],"names":["NODE_ROOT_ID","authorHtmlToMarkdown","inlineAuthorHtmlToMarkdown","resolveMarkdownSourceLinks","absoluteMediaSrc","SAFE_HREF_PATTERN","resolveScreenHref","splitLinkValue","elementOf","node","String","props","toLowerCase","CHROME_COMPONENTS","Set","isPageChromeNode","has","componentId","element","HEADING","headingLevelOf","asElement","exec","Number","variant","asVariant","escapeMarkdownText","value","replace","collapse","trim","absoluteHref","href","context","test","startsWith","origin","labelOf","html","rich","children","walk","nodes","rootId","blocks","seen","depth","add","childIds","Array","isArray","filter","id","recurse","childId","push","text","level","repeat","content","authoredMarkdown","src","hostId","alt","label","screenLinkHref","items","lines","map","item","Boolean","join","target","undefined","screenId","path","screenRoutes","source","reference","pageContentRootId","buildPageMarkdown","options","front","parts","title","description","meta","author","publishedAt","updatedAt","length","body","contentRoot","block","canonicalUrl"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/page-markdown.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A published page as Markdown — the representation an agent gets when it\n * sends `Accept: text/markdown` (acceptmarkdown.com).\n *\n * ## Why serialize the NODE TREE and not the rendered HTML\n *\n * The obvious implementation is to render the page and run an HTML-to-Markdown\n * converter over it. Every site on this platform is a node document, and the\n * tree carries what the HTML has already thrown away: which text is a heading\n * because the author said so rather than because it is large, which container\n * is the page's content region, and which subtree is navigation. Converting\n * the HTML back would be reconstructing, by heuristic, information we are\n * holding.\n *\n * It is also cheaper by a wide margin — no React render, no plugin registry,\n * no MUI — which is what makes this affordable to serve from a cached route on\n * every site rather than only where somebody enabled it.\n *\n * ## The content region, not the document\n *\n * Serialization starts at the document's `main` landmark, which\n * `stampDocumentLandmark` has already placed on the layout slot (the page\n * content between the chrome) or on the screen root. That is what makes the\n * output nav-free and footer-free without this module knowing anything about\n * how a particular site builds its header — the same property that makes a\n * Markdown variant worth serving at all.\n *\n * A page with no landmark falls back to the whole tree. Some content is always\n * better than an empty document, and a page that shipped no landmark\n * deliberately (see `document-landmark.ts`) has no content region to prefer.\n *\n * ## Chrome that lives INSIDE the content region\n *\n * A handful of components are navigation wherever they sit — an app bar, a nav\n * menu, a language switcher, a pagination strip. They are skipped by id, and\n * the list is deliberately short: everything not named recurses, so a plugin\n * block nobody here has heard of still contributes its text instead of\n * vanishing. Silence is the one failure mode that would be invisible to the\n * author.\n */\n\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport { authorHtmlToMarkdown, inlineAuthorHtmlToMarkdown } from './author-html-markdown'\nimport { resolveMarkdownSourceLinks } from './markdown-lite'\nimport { absoluteMediaSrc } from './media-ref'\nimport {\n SAFE_HREF_PATTERN,\n resolveScreenHref,\n splitLinkValue,\n} from './screen-link-value'\n\n/** The node shape this serializer reads; keeps callers free of the full type. */\nexport interface PageMarkdownNode {\n componentId?: string\n props?: Record<string, unknown> | null\n /** Normalized documents carry child IDS; this never reads a denormalized tree. */\n nodes?: unknown\n}\n\n/** A flat node map keyed by node id — exactly what the page route already holds. */\nexport type PageMarkdownNodes = Record<string, PageMarkdownNode>\n\n/** The element name a node was told to render as (`props.component`). */\nconst elementOf = (node: PageMarkdownNode | undefined): string =>\n String(node?.props?.['component'] ?? '').toLowerCase()\n\n/** Components that are navigation wherever they appear. See the module note. */\nconst CHROME_COMPONENTS: ReadonlySet<string> = new Set([\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiBreadcrumbs',\n 'navMenu',\n 'searchBox',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'muiPagination',\n // An icon is decoration with no accessible text of its own. Its LABEL, where\n // one exists, belongs to the control around it and is emitted there.\n 'icon',\n])\n\n/**\n * Whether a node is navigation rather than content: a chrome component, or\n * an element its author declared `nav`, `header` or `footer`. Exported so a\n * reader of a page's CONTENT — the SEO audit's heading and image scan\n * (AGL-2910) — skips exactly what this serializer skips.\n */\nexport function isPageChromeNode(node: PageMarkdownNode | undefined): boolean {\n if (!node) return false\n if (CHROME_COMPONENTS.has(String(node.componentId ?? ''))) return true\n const element = elementOf(node)\n return element === 'nav' || element === 'header' || element === 'footer'\n}\n\n/** Heading levels, as either `props.component` or `props.variant` spells them. */\nconst HEADING = /^h([1-6])$/\n\n/**\n * The heading level a text node publishes, or 0 for body copy.\n *\n * `component` wins over `variant` because they answer different questions and\n * the platform lets them disagree: `variant` is the type STYLE (what size it is\n * drawn at) and `component` is the ELEMENT (what it means). An author who set a\n * hero line to look like an h1 but render as a `p` said it is not a heading,\n * and the Markdown has to agree with the HTML or the two documents describe\n * different pages.\n */\nexport function headingLevelOf(props: Record<string, unknown> | null | undefined): number {\n const element = String(props?.['component'] ?? '').toLowerCase()\n if (element) {\n const asElement = HEADING.exec(element)\n return asElement ? Number(asElement[1]) : 0\n }\n const variant = String(props?.['variant'] ?? '').toLowerCase()\n const asVariant = HEADING.exec(variant)\n return asVariant ? Number(asVariant[1]) : 0\n}\n\n/**\n * Escape the Markdown control characters that would otherwise re-parse.\n *\n * Only the characters that START a construct at the position they can occur in:\n * a leading `#`, `>`, `-`, `+` or `1.` on its own line, and the emphasis,\n * backtick and bracket characters anywhere. Escaping more than that produces\n * output littered with backslashes, which is worse to read and worse to embed\n * than the rare false heading it prevents.\n */\nexport function escapeMarkdownText(value: string): string {\n return value\n .replace(/([\\\\`*_[\\]<>])/g, '\\\\$1')\n .replace(/^(\\s*)([#>])/gm, '$1\\\\$2')\n .replace(/^(\\s*)([-+])(\\s)/gm, '$1\\\\$2$3')\n .replace(/^(\\s*)(\\d+)\\.(\\s)/gm, '$1$2\\\\.$3')\n}\n\n/** Collapse runs of whitespace to single spaces, as inline text renders. */\nconst collapse = (value: string): string => value.replace(/\\s+/g, ' ').trim()\n\n/** Resolution context for values that are stored as references, not URLs. */\nexport interface PageMarkdownContext {\n /** Site origin, for absolutizing media and site-relative links. */\n origin?: string | null\n /** Host id, so a restricted org asset resolves for the site rendering it. */\n hostId?: string\n /**\n * The routing map screen links resolve against (`screenRoutes`), and the\n * link references in authored markdown with them (AGL-3118).\n */\n screenRoutes?: Record<string, string> | null\n}\n\n/**\n * A link target as Markdown should carry it: absolute where we can make it so.\n *\n * Site-relative is fine in a page — the browser has a document to resolve\n * against. A Markdown body has no such context by the time an agent has copied\n * it into a prompt, so a relative href there is a link to nowhere.\n */\nfunction absoluteHref(href: string, context?: PageMarkdownContext): string {\n const value = href.trim()\n if (!value) return ''\n if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith('//')) return value\n const origin = context?.origin?.replace(/\\/+$/, '') ?? ''\n if (!origin) return value\n return value.startsWith('/') ? `${origin}${value}` : `${origin}/${value}`\n}\n\n/** The text a control shows, preferring its rich label over the plain one. */\nfunction labelOf(\n props: Record<string, unknown> | null | undefined,\n context?: PageMarkdownContext,\n): string {\n const html = props?.['html']\n if (typeof html === 'string' && html.trim()) {\n const rich = inlineAuthorHtmlToMarkdown(html, context)\n if (rich.trim()) return collapse(rich)\n }\n const children = props?.['children']\n return typeof children === 'string' ? collapse(escapeMarkdownText(children)) : ''\n}\n\n/** A block emitted by the walk, before the blocks are joined. */\ninterface Block {\n /** Markdown text of the block, already escaped. */\n text: string\n}\n\n/**\n * Serialize the subtree at `rootId` into Markdown blocks.\n *\n * A depth budget and a visited set, because a node map is data: a document\n * restored from a backup, or written through the REST API, can name a child\n * that names it back. Recursing that tree is a stack overflow inside a request\n * handler, i.e. a 500 on a page that renders fine — the renderer tolerates it\n * because React unmounts a cycle, and this has nothing to stop it but itself.\n */\nfunction walk(\n nodes: PageMarkdownNodes,\n rootId: string,\n context: PageMarkdownContext | undefined,\n blocks: Block[],\n seen: Set<string>,\n depth: number,\n): void {\n if (depth > 64 || seen.has(rootId)) return\n const node = nodes[rootId]\n if (!node) return\n seen.add(rootId)\n\n const componentId = String(node.componentId ?? '')\n // Chrome components, and `nav`, `header` and `footer` by their OWN\n // declaration — an author who set the element picker to one of them named\n // it navigation, and that is a stronger signal than any component-id list\n // can be.\n if (isPageChromeNode(node)) return\n\n const props = node.props ?? {}\n const childIds = Array.isArray(node.nodes)\n ? node.nodes.filter((id): id is string => typeof id === 'string')\n : []\n const recurse = (): void => {\n for (const childId of childIds) {\n walk(nodes, childId, context, blocks, seen, depth + 1)\n }\n }\n const push = (text: string): void => {\n if (text.trim()) blocks.push({ text: text.trim() })\n }\n\n switch (componentId) {\n case 'muiTypography':\n case 'muiInlineText':\n case 'muiListItemText': {\n const level = headingLevelOf(props)\n const text = labelOf(props, context)\n if (!text) {\n // A Typography with no text of its own but children beneath it is a\n // wrapper, which is how rich composites are built. Recurse rather than\n // dropping the subtree.\n recurse()\n return\n }\n push(level > 0 ? `${'#'.repeat(level)} ${text}` : text)\n // A Typography renders the elements it holds after its text\n // (AGL-3672), so they read after it here too.\n recurse()\n return\n }\n case 'markdown': {\n /*\n Already Markdown. Emitted VERBATIM and never re-escaped: `content` is\n the markdown-lite source an author typed, so escaping it would turn\n their headings into literal `#` characters — the one transformation\n that can only make this output worse. Its link REFERENCES are the one\n exception (AGL-3118), resolved as the page resolves them.\n */\n const content = props['content']\n if (typeof content === 'string') push(authoredMarkdown(content, context))\n return\n }\n case 'custom-html': {\n const html = props['html'] ?? props['children']\n if (typeof html === 'string') push(authorHtmlToMarkdown(html, context))\n return\n }\n case 'image': {\n const src = absoluteMediaSrc(String(props['src'] ?? '').trim(), {\n hostId: context?.hostId,\n origin: context?.origin,\n })\n if (!src) return\n const alt = collapse(String(props['alt'] ?? ''))\n push(``)\n return\n }\n case 'video':\n case 'videoEmbed': {\n const src = String(props['src'] ?? props['url'] ?? '').trim()\n if (!src) return\n push(`[Video](${absoluteHref(src, context)})`)\n return\n }\n case 'muiButton':\n case 'muiScreenLink': {\n const label = labelOf(props, context) || 'Link'\n const href = screenLinkHref(props, context)\n push(href ? `[${label}](${href})` : label)\n return\n }\n case 'muiAccordionSummary': {\n /*\n An accordion's question (AGL-3575). The summary carries its text in\n its own props, not in a child node, so the default recursion below\n emitted NOTHING for it — every FAQ on every site reached an agent as\n a run of orphaned answers. It is a heading because that is what the\n HTML publishes: MUI renders the summary inside its `h3` heading slot,\n and the Markdown has to agree with the page or the two describe\n different documents. A linked header (AGL-1232) keeps its link, as\n the page does.\n */\n const text = labelOf(props, context)\n if (!text) {\n recurse()\n return\n }\n const href =\n typeof props['screenId'] === 'string' && props['screenId']\n ? screenLinkHref(props, context)\n : ''\n push(href ? `### [${text}](${href})` : `### ${text}`)\n return\n }\n case 'muiList': {\n /*\n The items are gathered into ONE block so they join as a list. Walking\n them as siblings would put a blank line between every bullet, which is\n a sequence of paragraphs beginning with a dash rather than a list.\n */\n const items: Block[] = []\n for (const childId of childIds) {\n walk(nodes, childId, context, items, seen, depth + 1)\n }\n const lines = items\n .map((item) => item.text.replace(/\\n/g, '\\n '))\n .filter(Boolean)\n .map((text) => `- ${text}`)\n push(lines.join('\\n'))\n return\n }\n default:\n recurse()\n }\n}\n\n/**\n * Where a Button or Screen Link points.\n *\n * A screen link stores a SCREEN ID, not a path, and the path it resolves to is\n * a per-request question — `screenRoutes` is the routing map the router\n * actually honors, which is not the map publishing wrote (a collection's list\n * template answers at the collection's slug, template screens answer nowhere).\n * Emitting `href` alone would publish, in Markdown, the dead links AGL-1998\n * removed from the HTML.\n *\n * `splitLinkValue` + `resolveScreenHref` rather than a local reading of the\n * two props, because WHICH SLOT a screen reference arrives in is not fixed\n * (AGL-1335): a `Link`-typed component prop is bound to whichever of\n * `screenId`/`href` the author reached for, and a value in the `href` slot\n * carries the stored `screen:<id>` spelling. Reading `screenId` alone published\n * that token verbatim as a link target on every page built from a prop-fed\n * CTA — ten of them on aglyn.com, unfollowable by the agents `/llms.txt` sends\n * here (AGL-2740). These are the same two functions `useLinkTarget` resolves\n * the HTML with, so the two representations of a page cannot drift again.\n *\n * An authored screen id that the map has lost resolves to NO link, exactly as\n * the HTML renders it: falling back to the stored `href` there would re-publish\n * the dead path AGL-1998 removed, which is the failure this function's own\n * contract exists to prevent.\n */\nfunction screenLinkHref(\n props: Record<string, unknown>,\n context?: PageMarkdownContext,\n): string {\n const target = splitLinkValue(\n typeof props['screenId'] === 'string' ? props['screenId'] : undefined,\n typeof props['href'] === 'string' ? props['href'] : undefined,\n )\n if (target.screenId) {\n const path = resolveScreenHref(context?.screenRoutes ?? undefined, target.screenId)\n return path ? absoluteHref(path, context) : ''\n }\n // The same protocol guard the linking elements apply. A `javascript:` href\n // is inert in a Markdown body, but it is still a target an agent may be\n // asked to follow, and a representation that offers one the page refuses to\n // render is describing a different document.\n return target.href && SAFE_HREF_PATTERN.test(target.href)\n ? absoluteHref(target.href, context)\n : ''\n}\n\n/**\n * Authored markdown-lite — a Markdown element's content, an entry's body —\n * as this document carries it: verbatim, except that each link REFERENCE\n * (AGL-3118) points where the page's own link does, through the same routing\n * map and the same `resolveScreenHref` as {@link screenLinkHref}, made\n * absolute for the reason {@link absoluteHref} gives. A reference the map\n * does not have becomes the link's text, as the page renders it: the stored\n * `entry:…` is not a URL any reader can follow.\n */\nfunction authoredMarkdown(\n source: string,\n context?: PageMarkdownContext,\n): string {\n return resolveMarkdownSourceLinks(source, (reference) => {\n const path = resolveScreenHref(context?.screenRoutes ?? undefined, reference)\n return path ? absoluteHref(path, context) : undefined\n })\n}\n\n/**\n * The node the document's content begins at.\n *\n * The `main` landmark first — `stampDocumentLandmark` puts it on the layout\n * slot or the screen root — then the layout slot by component id, then the\n * document root. Each fallback is wider than the last, so a tree that is\n * missing the marker still serializes rather than returning nothing.\n */\nexport function pageContentRootId(\n nodes: PageMarkdownNodes | null | undefined,\n rootId: string = NODE_ROOT_ID,\n): string | null {\n if (!nodes) return null\n for (const id in nodes) {\n if (elementOf(nodes[id]) === 'main') return id\n }\n for (const id in nodes) {\n if (nodes[id]?.componentId === 'layoutSlot') return id\n }\n return nodes[rootId] ? rootId : null\n}\n\n/** What a Markdown page carries above its body. */\nexport interface PageMarkdownFrontMatter {\n /** The `# ` heading. The page's own title, never the site's. */\n title?: string\n /** The meta description, emitted as the summary blockquote. */\n description?: string\n /** Canonical absolute URL, emitted as the source line. */\n canonicalUrl?: string\n /** ISO-8601 publication date, for a content entry. */\n publishedAt?: string\n /** ISO-8601 last-modified date, for a content entry. */\n updatedAt?: string\n /** Byline, for a content entry. */\n author?: string\n}\n\n/**\n * The whole Markdown representation of a page.\n *\n * @param body - already-Markdown body to use INSTEAD of walking nodes; the\n * content-entry case, where the stored body is markdown-lite source and\n * round-tripping it through the node tree could only lose fidelity.\n */\nexport function buildPageMarkdown(options: {\n nodes?: PageMarkdownNodes | null\n rootId?: string\n body?: string | null\n front?: PageMarkdownFrontMatter\n context?: PageMarkdownContext\n}): string {\n const front = options.front ?? {}\n const parts: string[] = []\n\n const title = collapse(String(front.title ?? ''))\n if (title) parts.push(`# ${escapeMarkdownText(title)}`)\n\n const description = collapse(String(front.description ?? ''))\n if (description) parts.push(`> ${escapeMarkdownText(description)}`)\n\n /*\n The dateline is one line rather than YAML front matter. Front matter is\n unparsed text to every Markdown reader that has not opted into it, so it\n arrives in an agent's context as a block of `key: value` noise above the\n content; a sentence is read correctly by all of them.\n */\n const meta: string[] = []\n if (front.author) meta.push(`By ${collapse(front.author)}`)\n if (front.publishedAt) meta.push(`Published ${front.publishedAt}`)\n if (front.updatedAt && front.updatedAt !== front.publishedAt) {\n meta.push(`Updated ${front.updatedAt}`)\n }\n if (meta.length) parts.push(`_${meta.join(' · ')}_`)\n\n if (typeof options.body === 'string' && options.body.trim()) {\n parts.push(authoredMarkdown(options.body.trim(), options.context))\n } else if (options.nodes) {\n const contentRoot = pageContentRootId(options.nodes, options.rootId)\n if (contentRoot) {\n const blocks: Block[] = []\n walk(options.nodes, contentRoot, options.context, blocks, new Set(), 0)\n for (const block of blocks) parts.push(block.text)\n }\n }\n\n if (front.canonicalUrl) {\n // A source line, and the reason it is worth the two lines it costs: an\n // agent that has copied this body into a prompt has no URL bar, so without\n // it the document cannot be cited, re-fetched, or linked to by whatever\n // reads it next.\n parts.push('---', `Source: ${front.canonicalUrl}`)\n }\n\n /*\n Collapse the runs of blank lines a nested walk leaves behind, and end with\n exactly one newline. A Markdown body is compared byte-for-byte by caches\n and by the conformance checkers agents run, so \"tidy\" here is a contract\n rather than an aesthetic.\n */\n return `${parts.join('\\n\\n').replace(/\\n{3,}/g, '\\n\\n').trim()}\\n`\n}\n"],"names":["NODE_ROOT_ID","authorHtmlToMarkdown","inlineAuthorHtmlToMarkdown","resolveMarkdownSourceLinks","absoluteMediaSrc","SAFE_HREF_PATTERN","resolveScreenHref","splitLinkValue","elementOf","node","String","props","toLowerCase","CHROME_COMPONENTS","Set","isPageChromeNode","has","componentId","element","HEADING","headingLevelOf","asElement","exec","Number","variant","asVariant","escapeMarkdownText","value","replace","collapse","trim","absoluteHref","href","context","test","startsWith","origin","labelOf","html","rich","children","walk","nodes","rootId","blocks","seen","depth","add","childIds","Array","isArray","filter","id","recurse","childId","push","text","level","repeat","content","authoredMarkdown","src","hostId","alt","label","screenLinkHref","items","lines","map","item","Boolean","join","target","undefined","screenId","path","screenRoutes","source","reference","pageContentRootId","buildPageMarkdown","options","front","parts","title","description","meta","author","publishedAt","updatedAt","length","body","contentRoot","block","canonicalUrl"],"mappings":"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"}
|
|
@@ -913,6 +913,15 @@ export declare const CONSOLE_WIDGET_SLOTS: {
|
|
|
913
913
|
* them; nothing a widget proposes reaches the published site before that.
|
|
914
914
|
*/
|
|
915
915
|
readonly hostSeo: "hostSeo";
|
|
916
|
+
/**
|
|
917
|
+
* The host setup Business profile section, under the site's profile and
|
|
918
|
+
* the facts read from its settings (AGL-3661). Props:
|
|
919
|
+
* {@link ConsoleHostBusinessProfileZoneProps}. What a plugin keeps ABOUT the
|
|
920
|
+
* business — the preferences an AI plugin learned from the owner's edits —
|
|
921
|
+
* shown where the owner reads and corrects the profile itself. A widget
|
|
922
|
+
* here writes only its own records.
|
|
923
|
+
*/
|
|
924
|
+
readonly hostBusinessProfile: "hostBusinessProfile";
|
|
916
925
|
/** {@link ConsoleRecordInsightsZoneProps} */
|
|
917
926
|
readonly recordInsights: "recordInsights";
|
|
918
927
|
/** {@link ConsoleRecordEmailZoneProps} */
|
|
@@ -1032,6 +1041,16 @@ export interface ConsoleHostScreenRowZoneProps {
|
|
|
1032
1041
|
/** The page's `kind` as stored. */
|
|
1033
1042
|
screenKind?: string;
|
|
1034
1043
|
}
|
|
1044
|
+
/** What the `hostBusinessProfile` zone hands each widget (AGL-3661). */
|
|
1045
|
+
export interface ConsoleHostBusinessProfileZoneProps {
|
|
1046
|
+
hostId: string;
|
|
1047
|
+
/** The org the page names; `undefined` while it resolves. */
|
|
1048
|
+
orgId: string | undefined;
|
|
1049
|
+
/** Path slug for building `/[orgSlug]/…` links. */
|
|
1050
|
+
orgSlug: string;
|
|
1051
|
+
/** The site's subdomain, which is what a console URL names a site by. */
|
|
1052
|
+
host: string | null;
|
|
1053
|
+
}
|
|
1035
1054
|
/** What the `hostScreens` zone hands each widget (AGL-2907). */
|
|
1036
1055
|
export interface ConsoleHostScreensZoneProps {
|
|
1037
1056
|
hostId: string;
|
|
@@ -428,6 +428,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
428
428
|
* which puts values in the form as unsaved edits. The form's Update stores
|
|
429
429
|
* them; nothing a widget proposes reaches the published site before that.
|
|
430
430
|
*/ hostSeo: 'hostSeo',
|
|
431
|
+
/**
|
|
432
|
+
* The host setup Business profile section, under the site's profile and
|
|
433
|
+
* the facts read from its settings (AGL-3661). Props:
|
|
434
|
+
* {@link ConsoleHostBusinessProfileZoneProps}. What a plugin keeps ABOUT the
|
|
435
|
+
* business — the preferences an AI plugin learned from the owner's edits —
|
|
436
|
+
* shown where the owner reads and corrects the profile itself. A widget
|
|
437
|
+
* here writes only its own records.
|
|
438
|
+
*/ hostBusinessProfile: 'hostBusinessProfile',
|
|
431
439
|
/** {@link ConsoleRecordInsightsZoneProps} */ recordInsights: 'recordInsights',
|
|
432
440
|
/** {@link ConsoleRecordEmailZoneProps} */ recordEmail: 'recordEmail',
|
|
433
441
|
/** {@link ConsoleImportMappingZoneProps} */ importMapping: 'importMapping',
|