@platformos/platformos-common 0.0.11 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/documents-locator/DocumentsLocator.d.ts +46 -2
  3. package/dist/documents-locator/DocumentsLocator.js +167 -1
  4. package/dist/documents-locator/DocumentsLocator.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/route-table/RouteTable.d.ts +45 -0
  9. package/dist/route-table/RouteTable.js +384 -0
  10. package/dist/route-table/RouteTable.js.map +1 -0
  11. package/dist/route-table/index.d.ts +5 -0
  12. package/dist/route-table/index.js +13 -0
  13. package/dist/route-table/index.js.map +1 -0
  14. package/dist/route-table/parseSlug.d.ts +31 -0
  15. package/dist/route-table/parseSlug.js +124 -0
  16. package/dist/route-table/parseSlug.js.map +1 -0
  17. package/dist/route-table/slugFromFilePath.d.ts +23 -0
  18. package/dist/route-table/slugFromFilePath.js +81 -0
  19. package/dist/route-table/slugFromFilePath.js.map +1 -0
  20. package/dist/route-table/types.d.ts +30 -0
  21. package/dist/route-table/types.js +3 -0
  22. package/dist/route-table/types.js.map +1 -0
  23. package/dist/translation-provider/TranslationProvider.d.ts +1 -1
  24. package/dist/translation-provider/TranslationProvider.js +2 -2
  25. package/dist/translation-provider/TranslationProvider.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/src/documents-locator/DocumentsLocator.spec.ts +367 -5
  29. package/src/documents-locator/DocumentsLocator.ts +197 -1
  30. package/src/index.ts +1 -0
  31. package/src/route-table/RouteTable.spec.ts +708 -0
  32. package/src/route-table/RouteTable.ts +437 -0
  33. package/src/route-table/index.ts +5 -0
  34. package/src/route-table/parseSlug.spec.ts +177 -0
  35. package/src/route-table/parseSlug.ts +136 -0
  36. package/src/route-table/slugFromFilePath.spec.ts +84 -0
  37. package/src/route-table/slugFromFilePath.ts +84 -0
  38. package/src/route-table/types.ts +25 -0
  39. package/src/translation-provider/TranslationProvider.ts +4 -2
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RouteTable = void 0;
7
+ const js_yaml_1 = __importDefault(require("js-yaml"));
8
+ const vscode_uri_1 = require("vscode-uri");
9
+ const AbstractFileSystem_1 = require("../AbstractFileSystem");
10
+ const path_utils_1 = require("../path-utils");
11
+ const slugFromFilePath_1 = require("./slugFromFilePath");
12
+ const parseSlug_1 = require("./parseSlug");
13
+ function extractFrontmatter(source) {
14
+ const trimmed = source.trimStart();
15
+ if (!trimmed.startsWith('---'))
16
+ return null;
17
+ const end = trimmed.indexOf('---', 3);
18
+ if (end === -1)
19
+ return null;
20
+ const yamlBlock = trimmed.slice(3, end).trim();
21
+ if (yamlBlock.length === 0)
22
+ return null;
23
+ try {
24
+ const result = js_yaml_1.default.load(yamlBlock);
25
+ if (typeof result !== 'object' || result === null)
26
+ return null;
27
+ return result;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Extract the path relative to the pages directory from a full URI.
35
+ * Handles both app-level and module page paths.
36
+ *
37
+ * App-level: matches `/(app|marketplace_builder)/(views/pages|pages)/`
38
+ * Module-level: matches `/(public|private)/(views/pages|pages)/`
39
+ * (marketplace_builder module pages always go through public/private subdirs,
40
+ * so the second pattern covers them.)
41
+ *
42
+ * Expects a `file://` URI with forward slashes (as produced by `vscode-uri`).
43
+ * On Windows, `vscode-uri` normalizes filesystem paths to forward-slash URIs
44
+ * (`file:///C:/...`), so the forward-slash regex patterns work cross-platform.
45
+ *
46
+ * Examples:
47
+ * file:///project/app/views/pages/about.html.liquid -> about.html.liquid
48
+ * file:///project/modules/admin/public/views/pages/dashboard.html.liquid -> dashboard.html.liquid
49
+ */
50
+ function extractRelativePagePath(uri) {
51
+ const patterns = [
52
+ // App-level pages: app/views/pages/ or marketplace_builder/pages/
53
+ /\/(app|marketplace_builder)\/(views\/pages|pages)\//,
54
+ // Module pages: must be under modules/<name>/(public|private)/(views/pages|pages)/
55
+ /\/modules\/[^/]+\/(public|private)\/(views\/pages|pages)\//,
56
+ ];
57
+ for (const pattern of patterns) {
58
+ const match = uri.match(pattern);
59
+ if (match) {
60
+ const idx = uri.indexOf(match[0]) + match[0].length;
61
+ return uri.slice(idx);
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ function buildRouteEntry(uri, slug, method, format) {
67
+ const { requiredSegments, optionalGroups } = (0, parseSlug_1.parseSlug)(slug);
68
+ const precedence = (0, parseSlug_1.calculatePrecedence)(slug, format);
69
+ return {
70
+ slug,
71
+ method,
72
+ format,
73
+ uri,
74
+ requiredSegments,
75
+ optionalGroups,
76
+ precedence,
77
+ };
78
+ }
79
+ /**
80
+ * Try to match a list of URL segments against a list of route segments.
81
+ * Returns true if all URL segments are consumed and matched.
82
+ */
83
+ function matchSegments(urlSegments, routeSegments, urlStart) {
84
+ let ui = urlStart;
85
+ for (let ri = 0; ri < routeSegments.length; ri++) {
86
+ const rs = routeSegments[ri];
87
+ if (rs.type === 'wildcard') {
88
+ // Wildcard consumes all remaining segments but requires at least one
89
+ if (ui >= urlSegments.length)
90
+ return false;
91
+ return urlSegments.length;
92
+ }
93
+ if (ui >= urlSegments.length) {
94
+ // Ran out of URL segments but route still has more
95
+ return false;
96
+ }
97
+ const us = urlSegments[ui];
98
+ if (us.type === 'dynamic') {
99
+ // Liquid interpolation matches any route segment type
100
+ ui++;
101
+ continue;
102
+ }
103
+ // us.type === 'static'
104
+ if (rs.type === 'static') {
105
+ if (us.value !== rs.value)
106
+ return false;
107
+ ui++;
108
+ }
109
+ else if (rs.type === 'param') {
110
+ // :param matches any single static segment
111
+ ui++;
112
+ }
113
+ }
114
+ return ui;
115
+ }
116
+ /**
117
+ * Parse a URL pattern into segments and an optional format suffix.
118
+ *
119
+ * Following Rails/platformOS convention, a known format extension on the last
120
+ * segment (e.g., `/api/data.json`) is stripped and returned separately so that
121
+ * matching can filter by format.
122
+ */
123
+ function parseUrlPattern(pattern) {
124
+ // Strip leading and trailing slashes
125
+ let path = pattern.startsWith('/') ? pattern.slice(1) : pattern;
126
+ if (path.endsWith('/'))
127
+ path = path.slice(0, -1);
128
+ if (path === '')
129
+ return { segments: [], format: null };
130
+ const rawSegments = path.split('/');
131
+ let format = null;
132
+ // Check if the last segment has a known format extension (e.g., my-page.json)
133
+ const lastIdx = rawSegments.length - 1;
134
+ const lastSeg = rawSegments[lastIdx];
135
+ if (lastSeg !== ':_liquid_') {
136
+ const dotIdx = lastSeg.lastIndexOf('.');
137
+ if (dotIdx > 0) {
138
+ const ext = lastSeg.slice(dotIdx + 1);
139
+ if (slugFromFilePath_1.KNOWN_FORMATS.has(ext)) {
140
+ format = ext;
141
+ rawSegments[lastIdx] = lastSeg.slice(0, dotIdx);
142
+ }
143
+ }
144
+ }
145
+ const segments = rawSegments.map((seg) => {
146
+ if (seg === ':_liquid_') {
147
+ return { type: 'dynamic' };
148
+ }
149
+ return { type: 'static', value: seg };
150
+ });
151
+ return { segments, format };
152
+ }
153
+ class RouteTable {
154
+ constructor(fs) {
155
+ this.fs = fs;
156
+ this.routes = new Map(); // uri -> entries (a page can register multiple entries for index aliasing)
157
+ this._built = false;
158
+ }
159
+ /** Returns true if build() has completed at least once. */
160
+ isBuilt() {
161
+ return this._built;
162
+ }
163
+ async build(rootUri) {
164
+ this.routes.clear();
165
+ const pageUris = await this.discoverPageFiles(rootUri);
166
+ for (const uri of pageUris) {
167
+ try {
168
+ const content = await this.fs.readFile(uri);
169
+ this.addPageFromContent(uri, content);
170
+ }
171
+ catch {
172
+ // Skip files we can't read
173
+ }
174
+ }
175
+ this._built = true;
176
+ }
177
+ updateFile(uri, content) {
178
+ this.removeFile(uri);
179
+ this.addPageFromContent(uri, content);
180
+ }
181
+ removeFile(uri) {
182
+ this.routes.delete(uri);
183
+ }
184
+ /**
185
+ * Find all routes matching a URL pattern and optional method.
186
+ * The pattern can contain `:_liquid_` segments for Liquid interpolations.
187
+ * Results are sorted by precedence (highest priority first = lowest number).
188
+ *
189
+ * A known format extension on the last segment (e.g., `/api/data.json`)
190
+ * is stripped and used to filter routes by format. When no format extension
191
+ * is present (e.g., `/about`), only `html` routes match — following the
192
+ * platformOS/Rails convention where HTML is the default format and non-HTML
193
+ * formats require an explicit extension or Accept header.
194
+ */
195
+ match(urlPattern, method) {
196
+ const { segments: urlSegments, format } = parseUrlPattern(urlPattern);
197
+ const effectiveFormat = format ?? 'html';
198
+ const results = [];
199
+ for (const entries of this.routes.values()) {
200
+ for (const entry of entries) {
201
+ if (method && entry.method !== method)
202
+ continue;
203
+ if (entry.format !== effectiveFormat)
204
+ continue;
205
+ if (this.matchEntry(urlSegments, entry)) {
206
+ results.push(entry);
207
+ }
208
+ }
209
+ }
210
+ results.sort((a, b) => a.precedence - b.precedence);
211
+ return results;
212
+ }
213
+ hasMatch(urlPattern, method) {
214
+ const { segments: urlSegments, format } = parseUrlPattern(urlPattern);
215
+ const effectiveFormat = format ?? 'html';
216
+ for (const entries of this.routes.values()) {
217
+ for (const entry of entries) {
218
+ if (method && entry.method !== method)
219
+ continue;
220
+ if (entry.format !== effectiveFormat)
221
+ continue;
222
+ if (this.matchEntry(urlSegments, entry))
223
+ return true;
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+ /** Returns the total number of route entries (including index aliases). */
229
+ routeCount() {
230
+ let count = 0;
231
+ for (const entries of this.routes.values()) {
232
+ count += entries.length;
233
+ }
234
+ return count;
235
+ }
236
+ allRoutes() {
237
+ const all = [];
238
+ for (const entries of this.routes.values()) {
239
+ all.push(...entries);
240
+ }
241
+ return all.sort((a, b) => a.precedence - b.precedence);
242
+ }
243
+ matchEntry(urlSegments, entry) {
244
+ // Try matching with required segments only
245
+ const afterRequired = matchSegments(urlSegments, entry.requiredSegments, 0);
246
+ if (afterRequired !== false && afterRequired === urlSegments.length) {
247
+ return true;
248
+ }
249
+ // Try matching with required + optional groups progressively
250
+ if (afterRequired !== false && entry.optionalGroups.length > 0) {
251
+ return this.matchOptionalGroups(urlSegments, entry.optionalGroups, afterRequired);
252
+ }
253
+ return false;
254
+ }
255
+ /**
256
+ * Try optional groups left-to-right greedily — matching Rails' ActionDispatch::Journey
257
+ * semantics. Each group is tried in order; if it matches, its segments are consumed
258
+ * and the next group is attempted. If it doesn't match, it's skipped (optional).
259
+ *
260
+ * This greedy approach is correct because the platformOS backend converts slugs like
261
+ * `search(/:country)(/:city)` into Journey path strings and Journey matches
262
+ * left-to-right without backtracking.
263
+ */
264
+ matchOptionalGroups(urlSegments, groups, startIdx) {
265
+ let current = startIdx;
266
+ for (const group of groups) {
267
+ if (current === urlSegments.length) {
268
+ // Remaining optional groups can be omitted
269
+ return true;
270
+ }
271
+ const after = matchSegments(urlSegments, group, current);
272
+ if (after !== false) {
273
+ current = after;
274
+ if (current === urlSegments.length)
275
+ return true;
276
+ }
277
+ }
278
+ return current === urlSegments.length;
279
+ }
280
+ addPageFromContent(uri, content) {
281
+ const relativePath = extractRelativePagePath(uri);
282
+ if (!relativePath)
283
+ return;
284
+ const frontmatter = extractFrontmatter(content);
285
+ const fileFormat = (0, slugFromFilePath_1.formatFromFilePath)(relativePath);
286
+ const method = (frontmatter?.method || 'get').toLowerCase();
287
+ const format = frontmatter?.format || fileFormat;
288
+ let slug;
289
+ if (frontmatter?.slug !== undefined && frontmatter.slug !== null) {
290
+ slug = String(frontmatter.slug);
291
+ }
292
+ else {
293
+ slug = (0, slugFromFilePath_1.slugFromFilePath)(relativePath, format);
294
+ }
295
+ const entries = [];
296
+ // Primary route
297
+ entries.push(buildRouteEntry(uri, slug, method, format));
298
+ // Index aliasing: if slug was derived from stripping /index,
299
+ // also register the /index variant
300
+ if (!frontmatter?.slug) {
301
+ const slugWithIndex = slug + '/index';
302
+ // Only add alias if the raw derivation actually stripped /index
303
+ // (i.e., the file is named index.{format}.liquid or index.liquid)
304
+ const baseName = relativePath.split('/').pop() || '';
305
+ const isIndexFile = baseName === 'index.liquid' ||
306
+ baseName === `index.${format}.liquid` ||
307
+ baseName === `index.${format}`;
308
+ if (isIndexFile && slug !== '/') {
309
+ entries.push(buildRouteEntry(uri, slugWithIndex, method, format));
310
+ }
311
+ }
312
+ this.routes.set(uri, entries);
313
+ }
314
+ async discoverPageFiles(rootUri) {
315
+ const uris = [];
316
+ // App-level pages
317
+ const appPaths = (0, path_utils_1.getAppPaths)(path_utils_1.PlatformOSFileType.Page);
318
+ for (const basePath of appPaths) {
319
+ const baseUri = vscode_uri_1.Utils.joinPath(rootUri, basePath);
320
+ await this.walkDirectory(baseUri.toString(), uris);
321
+ }
322
+ // Module pages — discover module names first, then walk all in parallel
323
+ const moduleNames = await this.discoverModuleNames(rootUri);
324
+ const moduleWalks = [];
325
+ for (const moduleName of moduleNames) {
326
+ const modulePaths = (0, path_utils_1.getModulePaths)(path_utils_1.PlatformOSFileType.Page, moduleName);
327
+ for (const basePath of modulePaths) {
328
+ const baseUri = vscode_uri_1.Utils.joinPath(rootUri, basePath);
329
+ moduleWalks.push(this.walkDirectory(baseUri.toString(), uris));
330
+ }
331
+ }
332
+ await Promise.all(moduleWalks);
333
+ return uris;
334
+ }
335
+ async discoverModuleNames(rootUri) {
336
+ const names = new Set();
337
+ for (const prefix of ['app/modules', 'modules']) {
338
+ const dirUri = vscode_uri_1.Utils.joinPath(rootUri, prefix).toString();
339
+ try {
340
+ const entries = await this.fs.readDirectory(dirUri);
341
+ for (const [name, type] of entries) {
342
+ if (type === AbstractFileSystem_1.FileType.Directory) {
343
+ // Extract the module name from the entry.
344
+ // readDirectory may return a full URI (file:///…/modules/admin) or
345
+ // a bare name ("admin"). Use URI.parse().path to normalise, then
346
+ // take the last non-empty segment.
347
+ const parsed = vscode_uri_1.URI.parse(name).path || name;
348
+ const segments = parsed.split('/').filter((s) => s.length > 0);
349
+ const moduleName = segments[segments.length - 1];
350
+ if (moduleName)
351
+ names.add(moduleName);
352
+ }
353
+ }
354
+ }
355
+ catch {
356
+ // Directory doesn't exist
357
+ }
358
+ }
359
+ return Array.from(names);
360
+ }
361
+ async walkDirectory(dirUri, results) {
362
+ let entries;
363
+ try {
364
+ entries = await this.fs.readDirectory(dirUri);
365
+ }
366
+ catch {
367
+ return;
368
+ }
369
+ const subdirs = [];
370
+ for (const [name, type] of entries) {
371
+ if (type === AbstractFileSystem_1.FileType.Directory) {
372
+ subdirs.push(this.walkDirectory(name, results));
373
+ }
374
+ else if (type === AbstractFileSystem_1.FileType.File && name.endsWith('.liquid')) {
375
+ // All platformOS page files use the .liquid extension (e.g. page.html.liquid,
376
+ // page.json.liquid). Non-.liquid pages are not supported by the platform.
377
+ results.push(name);
378
+ }
379
+ }
380
+ await Promise.all(subdirs);
381
+ }
382
+ }
383
+ exports.RouteTable = RouteTable;
384
+ //# sourceMappingURL=RouteTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteTable.js","sourceRoot":"","sources":["../../src/route-table/RouteTable.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { RouteTable } from './RouteTable';
2
+ export { slugFromFilePath, formatFromFilePath, KNOWN_FORMATS } from './slugFromFilePath';
3
+ export { parseSlug, calculatePrecedence } from './parseSlug';
4
+ export type { RouteEntry, RouteSegment } from './types';
5
+ export type { ParsedSlug } from './parseSlug';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculatePrecedence = exports.parseSlug = exports.KNOWN_FORMATS = exports.formatFromFilePath = exports.slugFromFilePath = exports.RouteTable = void 0;
4
+ var RouteTable_1 = require("./RouteTable");
5
+ Object.defineProperty(exports, "RouteTable", { enumerable: true, get: function () { return RouteTable_1.RouteTable; } });
6
+ var slugFromFilePath_1 = require("./slugFromFilePath");
7
+ Object.defineProperty(exports, "slugFromFilePath", { enumerable: true, get: function () { return slugFromFilePath_1.slugFromFilePath; } });
8
+ Object.defineProperty(exports, "formatFromFilePath", { enumerable: true, get: function () { return slugFromFilePath_1.formatFromFilePath; } });
9
+ Object.defineProperty(exports, "KNOWN_FORMATS", { enumerable: true, get: function () { return slugFromFilePath_1.KNOWN_FORMATS; } });
10
+ var parseSlug_1 = require("./parseSlug");
11
+ Object.defineProperty(exports, "parseSlug", { enumerable: true, get: function () { return parseSlug_1.parseSlug; } });
12
+ Object.defineProperty(exports, "calculatePrecedence", { enumerable: true, get: function () { return parseSlug_1.calculatePrecedence; } });
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/route-table/index.ts"],"names":[],"mappings":";;;AAAA,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,uDAAyF;AAAhF,oHAAA,gBAAgB,OAAA;AAAE,sHAAA,kBAAkB,OAAA;AAAE,iHAAA,aAAa,OAAA;AAC5D,yCAA6D;AAApD,sGAAA,SAAS,OAAA;AAAE,gHAAA,mBAAmB,OAAA"}
@@ -0,0 +1,31 @@
1
+ import { RouteSegment } from './types';
2
+ export interface ParsedSlug {
3
+ requiredSegments: RouteSegment[];
4
+ optionalGroups: RouteSegment[][];
5
+ }
6
+ /**
7
+ * Parse a slug string into required segments and optional groups.
8
+ *
9
+ * Slug syntax:
10
+ * - `about` -> required: [static('about')]
11
+ * - `users/:id` -> required: [static('users'), param('id')]
12
+ * - `users(/:id)` -> required: [static('users')], optional: [[param('id')]]
13
+ * - `search(/:country)(/:city)` -> required: [static('search')], optional: [[param('country')], [param('city')]]
14
+ * - `users(/section/*)` -> required: [static('users')], optional: [[static('section'), wildcard('*')]]
15
+ * - `/` (root) -> required: [], optional: []
16
+ */
17
+ export declare function parseSlug(slug: string): ParsedSlug;
18
+ /**
19
+ * Calculate route precedence following the backend's scoring algorithm.
20
+ * Returns a negative number; more negative = higher priority.
21
+ * When sorting ascending, highest-priority routes come first.
22
+ *
23
+ * Segment weights:
24
+ * - Static/hardcoded: 100 points
25
+ * - Required parameter (:param): 10 points
26
+ * - Optional parameter (inside parens): 1 point
27
+ *
28
+ * Base: weighted_size * -100
29
+ * Adjustments: slug='/' +1, format='html' +1, format-in-last-component -1
30
+ */
31
+ export declare function calculatePrecedence(slug: string, format: string): number;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSlug = parseSlug;
4
+ exports.calculatePrecedence = calculatePrecedence;
5
+ /**
6
+ * Parse a single segment string (without slashes) into a RouteSegment.
7
+ * - Starts with `:` -> param
8
+ * - Starts with `*` -> wildcard
9
+ * - Otherwise -> static
10
+ */
11
+ function parseSegment(raw) {
12
+ // Strip trailing `)` if present (from optional group splitting)
13
+ const cleaned = raw.endsWith(')') ? raw.slice(0, -1) : raw;
14
+ if (cleaned.startsWith(':')) {
15
+ return { type: 'param', name: cleaned.slice(1) };
16
+ }
17
+ if (cleaned.startsWith('*')) {
18
+ return { type: 'wildcard', name: cleaned.slice(1) || '*' };
19
+ }
20
+ return { type: 'static', value: cleaned };
21
+ }
22
+ function parseSegments(part) {
23
+ return part
24
+ .split('/')
25
+ .filter((s) => s.length > 0)
26
+ .map(parseSegment);
27
+ }
28
+ /**
29
+ * Parse a slug string into required segments and optional groups.
30
+ *
31
+ * Slug syntax:
32
+ * - `about` -> required: [static('about')]
33
+ * - `users/:id` -> required: [static('users'), param('id')]
34
+ * - `users(/:id)` -> required: [static('users')], optional: [[param('id')]]
35
+ * - `search(/:country)(/:city)` -> required: [static('search')], optional: [[param('country')], [param('city')]]
36
+ * - `users(/section/*)` -> required: [static('users')], optional: [[static('section'), wildcard('*')]]
37
+ * - `/` (root) -> required: [], optional: []
38
+ */
39
+ function parseSlug(slug) {
40
+ // Root is special
41
+ if (slug === '/' || slug === '') {
42
+ return { requiredSegments: [], optionalGroups: [] };
43
+ }
44
+ // Split into required part and optional groups by finding `(`
45
+ const firstParen = slug.indexOf('(');
46
+ if (firstParen === -1) {
47
+ // No optional groups
48
+ return {
49
+ requiredSegments: parseSegments(slug),
50
+ optionalGroups: [],
51
+ };
52
+ }
53
+ // Required part is everything before the first `(`
54
+ const requiredPart = slug.slice(0, firstParen);
55
+ const optionalPart = slug.slice(firstParen);
56
+ const requiredSegments = requiredPart.length > 0 ? parseSegments(requiredPart) : [];
57
+ // Parse optional groups: split on `(` to get each group, strip `)` and leading `/`
58
+ const optionalGroups = [];
59
+ const groupRegex = /\(([^)]+)\)/g;
60
+ let match;
61
+ while ((match = groupRegex.exec(optionalPart)) !== null) {
62
+ const groupContent = match[1];
63
+ // Strip leading `/` if present
64
+ const normalized = groupContent.startsWith('/') ? groupContent.slice(1) : groupContent;
65
+ if (normalized.length > 0) {
66
+ optionalGroups.push(parseSegments(normalized));
67
+ }
68
+ }
69
+ return { requiredSegments, optionalGroups };
70
+ }
71
+ /**
72
+ * Calculate route precedence following the backend's scoring algorithm.
73
+ * Returns a negative number; more negative = higher priority.
74
+ * When sorting ascending, highest-priority routes come first.
75
+ *
76
+ * Segment weights:
77
+ * - Static/hardcoded: 100 points
78
+ * - Required parameter (:param): 10 points
79
+ * - Optional parameter (inside parens): 1 point
80
+ *
81
+ * Base: weighted_size * -100
82
+ * Adjustments: slug='/' +1, format='html' +1, format-in-last-component -1
83
+ */
84
+ function calculatePrecedence(slug, format) {
85
+ if (slug === '/' || slug === '') {
86
+ // Special case for root: weighted_size=0 -> use 1
87
+ let precedence = 1 * -100;
88
+ precedence += 1; // root "/" adjustment
89
+ if (format === 'html')
90
+ precedence += 1;
91
+ return precedence;
92
+ }
93
+ // Split on `(?/` boundary to get all segments with their weight context
94
+ // The Ruby code: slug.split(%r{\(?/}).inject(0) { ... }
95
+ const parts = slug.split(/\(?\//);
96
+ let weightedSize = 0;
97
+ for (const part of parts) {
98
+ if (part.length === 0)
99
+ continue;
100
+ if (part.startsWith(':')) {
101
+ weightedSize += part.endsWith(')') ? 1 : 10;
102
+ }
103
+ else if (part.startsWith('*')) {
104
+ weightedSize += part.endsWith(')') ? 1 : 10;
105
+ }
106
+ else {
107
+ weightedSize += 100;
108
+ }
109
+ }
110
+ let precedence = (weightedSize === 0 ? 1 : weightedSize) * -100;
111
+ if (format === 'html')
112
+ precedence += 1;
113
+ // Check if format is embedded in last slug component (e.g. `data.json`)
114
+ const lastSlash = slug.lastIndexOf('/');
115
+ const lastComponent = lastSlash >= 0 ? slug.slice(lastSlash + 1) : slug;
116
+ // Strip optional group parens for checking
117
+ const cleanLast = lastComponent.replace(/[()]/g, '');
118
+ const dotIdx = cleanLast.lastIndexOf('.');
119
+ if (dotIdx > 0 && dotIdx < cleanLast.length - 1) {
120
+ precedence -= 1;
121
+ }
122
+ return precedence;
123
+ }
124
+ //# sourceMappingURL=parseSlug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseSlug.js","sourceRoot":"","sources":["../../src/route-table/parseSlug.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Derives a URL slug from a page file path relative to the pages directory.
3
+ *
4
+ * Rules (ported from Ruby Page.default_routing_options):
5
+ * 1. Strip file extensions: first `.liquid`, then `.{format}` (e.g. `.html`)
6
+ * 2. If result is `index` -> slug = `/`
7
+ * 3. If result ends with `/index` -> strip it (e.g. `test/index` -> `test`)
8
+ * 4. If result is `home` -> slug = `/` (deprecated alias for root)
9
+ * 5. Otherwise slug = the remaining path
10
+ */
11
+ export declare function slugFromFilePath(relativeToPages: string, format?: string): string;
12
+ /**
13
+ * Known response formats supported by the platformOS engine.
14
+ * Derived from the platform's FORMAT_ENUM.
15
+ */
16
+ export declare const KNOWN_FORMATS: Set<string>;
17
+ /**
18
+ * Extracts the format from a page filename.
19
+ * Returns the format if the file has a double extension like `.json.liquid` or `.xml.liquid`
20
+ * and the extension is a known platformOS format.
21
+ * Returns 'html' as the default if only `.liquid` is present or the extension is unknown.
22
+ */
23
+ export declare function formatFromFilePath(relativeToPages: string): string;