@teleporthq/teleport-plugin-next-data-source 0.43.71 → 0.43.75

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 (143) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +8 -1
  2. package/__tests__/blog-adjacent-arranged.test.ts +176 -0
  3. package/__tests__/blog-post-contents.test.ts +212 -0
  4. package/__tests__/blog-post-page-enrichment.test.ts +28 -5
  5. package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
  6. package/__tests__/blog-post-reading-time.test.ts +55 -0
  7. package/__tests__/browser-row-policy.test.ts +5 -1
  8. package/__tests__/content-preset-wiring.test.ts +108 -0
  9. package/__tests__/fixed-list-sort.test.ts +183 -0
  10. package/__tests__/pagination-filter-groups.test.ts +186 -0
  11. package/__tests__/plain-list-sort.test.ts +215 -0
  12. package/__tests__/product-price-sort.ts +47 -19
  13. package/__tests__/rest-api-key-header.test.ts +18 -0
  14. package/__tests__/search-url-sync.test.ts +34 -15
  15. package/__tests__/shared-resource-prefetch.test.ts +65 -0
  16. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/postgresql.js +2 -1
  18. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  19. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/rest-api.js +3 -1
  21. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  22. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/teleport.js +3 -2
  24. package/dist/cjs/fetchers/teleport.js.map +1 -1
  25. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
  26. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  27. package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
  28. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  29. package/dist/cjs/filter-utils.d.ts +19 -7
  30. package/dist/cjs/filter-utils.d.ts.map +1 -1
  31. package/dist/cjs/filter-utils.js +49 -13
  32. package/dist/cjs/filter-utils.js.map +1 -1
  33. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  34. package/dist/cjs/pagination-plugin.js +137 -118
  35. package/dist/cjs/pagination-plugin.js.map +1 -1
  36. package/dist/cjs/product-price-sort.d.ts +0 -9
  37. package/dist/cjs/product-price-sort.d.ts.map +1 -1
  38. package/dist/cjs/product-price-sort.js +1 -13
  39. package/dist/cjs/product-price-sort.js.map +1 -1
  40. package/dist/cjs/row-order.d.ts +19 -0
  41. package/dist/cjs/row-order.d.ts.map +1 -0
  42. package/dist/cjs/row-order.js +45 -0
  43. package/dist/cjs/row-order.js.map +1 -0
  44. package/dist/cjs/sort-utils.d.ts +10 -0
  45. package/dist/cjs/sort-utils.d.ts.map +1 -1
  46. package/dist/cjs/sort-utils.js +22 -1
  47. package/dist/cjs/sort-utils.js.map +1 -1
  48. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
  49. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
  50. package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
  51. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
  52. package/dist/cjs/transformations/blog-comments.d.ts +1 -1
  53. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
  54. package/dist/cjs/transformations/blog-comments.js +1 -1
  55. package/dist/cjs/transformations/blog-comments.js.map +1 -1
  56. package/dist/cjs/transformations/blog-post.d.ts +14 -0
  57. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  58. package/dist/cjs/transformations/blog-post.js +4 -1
  59. package/dist/cjs/transformations/blog-post.js.map +1 -1
  60. package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
  61. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
  62. package/dist/cjs/transformations/heading-anchors.js +34 -10
  63. package/dist/cjs/transformations/heading-anchors.js.map +1 -1
  64. package/dist/cjs/transformations/index.d.ts +24 -12
  65. package/dist/cjs/transformations/index.d.ts.map +1 -1
  66. package/dist/cjs/transformations/index.js +59 -11
  67. package/dist/cjs/transformations/index.js.map +1 -1
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/cjs/utils.d.ts.map +1 -1
  70. package/dist/cjs/utils.js +52 -3
  71. package/dist/cjs/utils.js.map +1 -1
  72. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  73. package/dist/esm/fetchers/postgresql.js +3 -2
  74. package/dist/esm/fetchers/postgresql.js.map +1 -1
  75. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  76. package/dist/esm/fetchers/rest-api.js +3 -1
  77. package/dist/esm/fetchers/rest-api.js.map +1 -1
  78. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  79. package/dist/esm/fetchers/teleport.js +4 -3
  80. package/dist/esm/fetchers/teleport.js.map +1 -1
  81. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
  82. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  83. package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
  84. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  85. package/dist/esm/filter-utils.d.ts +19 -7
  86. package/dist/esm/filter-utils.d.ts.map +1 -1
  87. package/dist/esm/filter-utils.js +46 -12
  88. package/dist/esm/filter-utils.js.map +1 -1
  89. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  90. package/dist/esm/pagination-plugin.js +140 -121
  91. package/dist/esm/pagination-plugin.js.map +1 -1
  92. package/dist/esm/product-price-sort.d.ts +0 -9
  93. package/dist/esm/product-price-sort.d.ts.map +1 -1
  94. package/dist/esm/product-price-sort.js +0 -11
  95. package/dist/esm/product-price-sort.js.map +1 -1
  96. package/dist/esm/row-order.d.ts +19 -0
  97. package/dist/esm/row-order.d.ts.map +1 -0
  98. package/dist/esm/row-order.js +40 -0
  99. package/dist/esm/row-order.js.map +1 -0
  100. package/dist/esm/sort-utils.d.ts +10 -0
  101. package/dist/esm/sort-utils.d.ts.map +1 -1
  102. package/dist/esm/sort-utils.js +20 -0
  103. package/dist/esm/sort-utils.js.map +1 -1
  104. package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
  105. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
  106. package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
  107. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
  108. package/dist/esm/transformations/blog-comments.d.ts +1 -1
  109. package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
  110. package/dist/esm/transformations/blog-comments.js +1 -1
  111. package/dist/esm/transformations/blog-comments.js.map +1 -1
  112. package/dist/esm/transformations/blog-post.d.ts +14 -0
  113. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  114. package/dist/esm/transformations/blog-post.js +4 -1
  115. package/dist/esm/transformations/blog-post.js.map +1 -1
  116. package/dist/esm/transformations/heading-anchors.d.ts +32 -8
  117. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
  118. package/dist/esm/transformations/heading-anchors.js +33 -9
  119. package/dist/esm/transformations/heading-anchors.js.map +1 -1
  120. package/dist/esm/transformations/index.d.ts +24 -12
  121. package/dist/esm/transformations/index.d.ts.map +1 -1
  122. package/dist/esm/transformations/index.js +58 -11
  123. package/dist/esm/transformations/index.js.map +1 -1
  124. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  125. package/dist/esm/utils.d.ts.map +1 -1
  126. package/dist/esm/utils.js +52 -3
  127. package/dist/esm/utils.js.map +1 -1
  128. package/package.json +5 -5
  129. package/src/fetchers/postgresql.ts +11 -15
  130. package/src/fetchers/rest-api.ts +3 -1
  131. package/src/fetchers/teleport.ts +12 -16
  132. package/src/fetchers/utils/browser-row-policy.ts +10 -20
  133. package/src/filter-utils.ts +102 -28
  134. package/src/pagination-plugin.ts +211 -175
  135. package/src/product-price-sort.ts +0 -11
  136. package/src/row-order.ts +54 -0
  137. package/src/sort-utils.ts +25 -0
  138. package/src/transformations/blog-adjacent-posts.ts +89 -3
  139. package/src/transformations/blog-comments.ts +5 -5
  140. package/src/transformations/blog-post.ts +64 -13
  141. package/src/transformations/heading-anchors.ts +79 -16
  142. package/src/transformations/index.ts +80 -13
  143. package/src/utils.ts +74 -3
@@ -1 +1 @@
1
- {"version":3,"file":"sort-utils.d.ts","sourceRoot":"","sources":["../../src/sort-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAEjF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC,UAAU,CAAA;IACvB,KAAK,EAAE,KAAK,CAAC,UAAU,CAAA;IACvB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAyDD,eAAO,MAAM,kBAAkB,SACvB,eAAe,GAAG,mBAAmB,GAAG,SAAS,iBACxC,eAAe,GAAG,mBAAmB,GAAG,SAAS,KAC/D,cAAc,GAAG,SAkBnB,CAAA;AAiDD,eAAO,MAAM,gBAAgB,gBACd,MAAM,cAAc,EAAE,eAEtB,GAAG,EAAE,GAAG,SAAS,eACjB,cAAc,GAAG,SAAS,KACtC,IASF,CAAA"}
1
+ {"version":3,"file":"sort-utils.d.ts","sourceRoot":"","sources":["../../src/sort-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAEjF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC,UAAU,CAAA;IACvB,KAAK,EAAE,KAAK,CAAC,UAAU,CAAA;IACvB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAyDD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,SAClB,eAAe,GAAG,mBAAmB,GAAG,SAAS,iBACxC,eAAe,GAAG,mBAAmB,GAAG,SAAS,KAC/D;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,GAAG,SAc7C,CAAA;AAED,eAAO,MAAM,kBAAkB,SACvB,eAAe,GAAG,mBAAmB,GAAG,SAAS,iBACxC,eAAe,GAAG,mBAAmB,GAAG,SAAS,KAC/D,cAAc,GAAG,SAkBnB,CAAA;AAiDD,eAAO,MAAM,gBAAgB,gBACd,MAAM,cAAc,EAAE,eAEtB,GAAG,EAAE,GAAG,SAAS,eACjB,cAAc,GAAG,SAAS,KACtC,IASF,CAAA"}
@@ -32,7 +32,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
32
32
  return to.concat(ar || Array.prototype.slice.call(from));
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.appendSortsParam = exports.extractDynamicSort = void 0;
35
+ exports.appendSortsParam = exports.extractDynamicSort = exports.readFixedSort = void 0;
36
36
  var types = __importStar(require("@babel/types"));
37
37
  var teleport_plugin_common_1 = require("@teleporthq/teleport-plugin-common");
38
38
  var SKIP_IDENTIFIERS = new Set([
@@ -84,6 +84,27 @@ var toExpressionAndDeps = function (value) {
84
84
  collectIdentifiers(parsed, deps);
85
85
  return { expr: parsed, deps: Array.from(deps) };
86
86
  };
87
+ /**
88
+ * The `{ field, order }` of a sort the UIDL FIXES — a static column and a
89
+ * static direction (or none: ascending) — or undefined for one bound to state.
90
+ * A fixed sort is a static sort: it rides on the server-side first-page fetch
91
+ * and the client keeps that page, which a dynamic sort cannot let it do.
92
+ */
93
+ var readFixedSort = function (sort, sortDirection) {
94
+ var _a;
95
+ if ((sort === null || sort === void 0 ? void 0 : sort.type) !== 'static' || typeof sort.content !== 'string' || !sort.content.trim()) {
96
+ return undefined;
97
+ }
98
+ if (sortDirection &&
99
+ (sortDirection.type !== 'static' || typeof sortDirection.content !== 'string')) {
100
+ return undefined;
101
+ }
102
+ var direction = String((_a = sortDirection === null || sortDirection === void 0 ? void 0 : sortDirection.content) !== null && _a !== void 0 ? _a : '')
103
+ .trim()
104
+ .toLowerCase();
105
+ return { field: sort.content.trim(), order: direction.startsWith('desc') ? 'desc' : 'asc' };
106
+ };
107
+ exports.readFixedSort = readFixedSort;
87
108
  var extractDynamicSort = function (sort, sortDirection) {
88
109
  if (!sort) {
89
110
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"sort-utils.js","sourceRoot":"","sources":["../../src/sort-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sort-utils.js","sourceRoot":"","sources":["../../src/sort-utils.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,16 @@
11
11
  *
12
12
  * MUST mirror `findAdjacentBlogPosts` in teleport-gui
13
13
  * `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
14
+ *
15
+ * In Manual Order (`arranged`) a post walks the published posts of its
16
+ * PRIMARY category (the first of `category_ids`) the way the author arranged
17
+ * them: the ones with a place (`sort_order`) by it, then the rest newest
18
+ * first, ties by id — "previous" the one above it, "next" the one below. The
19
+ * category is read off each row in code, since a list cell may be JSON or a
20
+ * comma-separated list (see `array-overlap-sql.ts`). A post without a
21
+ * category, or a table the query cannot read (a column it lacks), walks the
22
+ * newest-first order above. MUST mirror `findArrangedAdjacentPosts` in
23
+ * teleport-gui `apps/gui/app/project-page/features/blog/utils/arranged-order.ts`.
14
24
  */
15
- export declare const generateAdjacentPostsCode: () => string;
25
+ export declare const generateAdjacentPostsCode: (postsTable: string, arranged?: boolean) => string;
16
26
  //# sourceMappingURL=blog-adjacent-posts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-adjacent-posts.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-adjacent-posts.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,QAAO,MAqD5C,CAAA"}
1
+ {"version":3,"file":"blog-adjacent-posts.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-adjacent-posts.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,yBAAyB,eAAgB,MAAM,yBAAqB,MAiIhF,CAAA"}
@@ -19,7 +19,20 @@ var ADJACENT_POST_CANDIDATES = 5;
19
19
  *
20
20
  * MUST mirror `findAdjacentBlogPosts` in teleport-gui
21
21
  * `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
22
+ *
23
+ * In Manual Order (`arranged`) a post walks the published posts of its
24
+ * PRIMARY category (the first of `category_ids`) the way the author arranged
25
+ * them: the ones with a place (`sort_order`) by it, then the rest newest
26
+ * first, ties by id — "previous" the one above it, "next" the one below. The
27
+ * category is read off each row in code, since a list cell may be JSON or a
28
+ * comma-separated list (see `array-overlap-sql.ts`). A post without a
29
+ * category, or a table the query cannot read (a column it lacks), walks the
30
+ * newest-first order above. MUST mirror `findArrangedAdjacentPosts` in
31
+ * teleport-gui `apps/gui/app/project-page/features/blog/utils/arranged-order.ts`.
22
32
  */
23
- var generateAdjacentPostsCode = function () { return "\nvar BLOG_ADJACENT_CANDIDATES = ".concat(ADJACENT_POST_CANDIDATES, "\n\nfunction blogAdjacentPostsSql(comparison, direction) {\n return (\n 'SELECT p.* FROM teleport_blog_posts p,' +\n ' (SELECT created_at, id FROM teleport_blog_posts WHERE id = $1) cur' +\n \" WHERE p.status = 'published' AND (p.created_at, p.id) \" + comparison + ' (cur.created_at, cur.id)' +\n ' ORDER BY p.created_at ' + direction + ', p.id ' + direction +\n ' LIMIT ' + BLOG_ADJACENT_CANDIDATES\n )\n}\n\nfunction firstLinkableBlogPost(result) {\n var rows = result && Array.isArray(result.rows) ? result.rows : []\n for (var i = 0; i < rows.length; i++) {\n var row = rows[i]\n if (!row) continue\n var redirect = pickFirst(row.redirect_url, row.redirectUrl)\n if (typeof redirect === 'string' && redirect.trim() !== '') continue\n // Through JSON, like the rows the fetcher itself hands the transform.\n return JSON.parse(JSON.stringify(row))\n }\n return null\n}\n\n// { [postId]: { previous, next } } \u2014 raw rows or null \u2014 for a single-record\n// fetch; {} otherwise and on any failure (the navigation then hides).\nasync function getAdjacentPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length !== 1) return map\n var postId = records[0] && records[0].id\n if (postId === null || postId === undefined) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])\n var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])\n map[String(postId)] = {\n previous: firstLinkableBlogPost(older),\n next: firstLinkableBlogPost(newer),\n }\n } catch (e) {\n // Leaves the navigation hidden.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n"); };
33
+ var generateAdjacentPostsCode = function (postsTable, arranged) {
34
+ if (arranged === void 0) { arranged = false; }
35
+ return "\nvar BLOG_ADJACENT_CANDIDATES = ".concat(ADJACENT_POST_CANDIDATES, "\nvar BLOG_ARRANGED_ORDER = ").concat(arranged, "\n\nfunction blogAdjacentPostsSql(comparison, direction) {\n return (\n 'SELECT p.* FROM ").concat(postsTable, " p,' +\n ' (SELECT created_at, id FROM ").concat(postsTable, " WHERE id = $1) cur' +\n \" WHERE p.status = 'published' AND (p.created_at, p.id) \" + comparison + ' (cur.created_at, cur.id)' +\n ' ORDER BY p.created_at ' + direction + ', p.id ' + direction +\n ' LIMIT ' + BLOG_ADJACENT_CANDIDATES\n )\n}\n\nfunction firstLinkableBlogPost(result) {\n var rows = result && Array.isArray(result.rows) ? result.rows : []\n for (var i = 0; i < rows.length; i++) {\n var row = rows[i]\n if (!row) continue\n var redirect = pickFirst(row.redirect_url, row.redirectUrl)\n if (typeof redirect === 'string' && redirect.trim() !== '') continue\n // Through JSON, like the rows the fetcher itself hands the transform.\n return JSON.parse(JSON.stringify(row))\n }\n return null\n}\n\nfunction blogPlaceOf(row) {\n var raw = pickFirst(row.sort_order, row.sortOrder)\n if (raw === null || raw === undefined || raw === '') return null\n var value = Number(raw)\n return isFinite(value) ? value : null\n}\n\n// The arranged posts by their place, then the rest newest first, ties by id descending.\nfunction compareBlogPlaces(a, b) {\n var placeA = blogPlaceOf(a)\n var placeB = blogPlaceOf(b)\n if (placeA !== placeB) {\n if (placeA === null) return 1\n if (placeB === null) return -1\n return placeA - placeB\n }\n var byDate =\n normalizeTimestamp(pickFirst(b.created_at, b.createdAt)) -\n normalizeTimestamp(pickFirst(a.created_at, a.createdAt))\n if (byDate !== 0) return byDate\n var idA = String(a.id)\n var idB = String(b.id)\n return idA < idB ? 1 : idA > idB ? -1 : 0\n}\n\n// { previous, next } along the post's primary category in Manual Order, or\n// null when the post has none (the caller then walks the newest-first order).\nasync function getArrangedAdjacentPosts(client, record) {\n var primary = parseJsonArray(record.category_ids)[0]\n if (!primary) return null\n var postId = String(record.id)\n var listed = await client.query(\n \"SELECT id, category_ids, sort_order, created_at FROM ").concat(postsTable, " WHERE status = 'published' OR id = $1\",\n [postId]\n )\n var chain = (listed && Array.isArray(listed.rows) ? listed.rows : [])\n .filter(function (row) {\n return String(row.id) === postId || parseJsonArray(row.category_ids)[0] === primary\n })\n .sort(compareBlogPlaces)\n var at = -1\n for (var i = 0; i < chain.length; i++) {\n if (String(chain[i].id) === postId) at = i\n }\n if (at < 0) return null\n var above = chain.slice(Math.max(0, at - BLOG_ADJACENT_CANDIDATES), at).reverse()\n var below = chain.slice(at + 1, at + 1 + BLOG_ADJACENT_CANDIDATES)\n var ids = above.concat(below).map(function (row) { return String(row.id) })\n if (ids.length === 0) return { previous: null, next: null }\n var full = await client.query(\n 'SELECT p.* FROM ").concat(postsTable, " p WHERE p.id::text = ANY($1::text[])',\n [ids]\n )\n var byId = {}\n ;(full && Array.isArray(full.rows) ? full.rows : []).forEach(function (row) {\n byId[String(row.id)] = row\n })\n var rowsOf = function (entries) {\n return { rows: entries.map(function (row) { return byId[String(row.id)] }) }\n }\n return { previous: firstLinkableBlogPost(rowsOf(above)), next: firstLinkableBlogPost(rowsOf(below)) }\n}\n\n// { [postId]: { previous, next } } \u2014 raw rows or null \u2014 for a single-record\n// fetch; {} otherwise and on any failure (the navigation then hides).\nasync function getAdjacentPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length !== 1) return map\n var postId = records[0] && records[0].id\n if (postId === null || postId === undefined) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var arranged = null\n if (BLOG_ARRANGED_ORDER) {\n try {\n arranged = await getArrangedAdjacentPosts(client, records[0])\n } catch (e) {\n // A table without the columns: the newest-first order below.\n }\n }\n if (arranged) {\n map[String(postId)] = arranged\n return map\n }\n var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])\n var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])\n map[String(postId)] = {\n previous: firstLinkableBlogPost(older),\n next: firstLinkableBlogPost(newer),\n }\n } catch (e) {\n // Leaves the navigation hidden.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n");
36
+ };
24
37
  exports.generateAdjacentPostsCode = generateAdjacentPostsCode;
25
38
  //# sourceMappingURL=blog-adjacent-posts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-adjacent-posts.js","sourceRoot":"","sources":["../../../src/transformations/blog-adjacent-posts.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,IAAM,wBAAwB,GAAG,CAAC,CAAA;AAElC;;;;;;;;;;;;;GAaG;AACI,IAAM,yBAAyB,GAAG,cAAc,OAAA,2CACtB,wBAAwB,g2DAoDxD,EArDsD,CAqDtD,CAAA;AArDY,QAAA,yBAAyB,6BAqDrC"}
1
+ {"version":3,"file":"blog-adjacent-posts.js","sourceRoot":"","sources":["../../../src/transformations/blog-adjacent-posts.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,IAAM,wBAAwB,GAAG,CAAC,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACI,IAAM,yBAAyB,GAAG,UAAC,UAAkB,EAAE,QAAgB;IAAhB,yBAAA,EAAA,gBAAgB;IAAa,OAAA,2CAC1D,wBAAwB,yCAC7B,QAAQ,0GAIb,UAAU,uDACG,UAAU,u9DAoDc,UAAU,8xBAkB/C,UAAU,ssDAoDhC;AAjI0F,CAiI1F,CAAA;AAjIY,QAAA,yBAAyB,6BAiIrC"}
@@ -26,5 +26,5 @@ export declare const BLOG_COMMENTS_RENDER_LIMIT = 200;
26
26
  * MUST mirror `buildBlogComments` in teleport-gui
27
27
  * `packages/renderer/src/utils/blog-comments.ts` (the canvas).
28
28
  */
29
- export declare const generateBlogCommentsCode: () => string;
29
+ export declare const generateBlogCommentsCode: (commentsTable: string) => string;
30
30
  //# sourceMappingURL=blog-comments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-comments.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-comments.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,QAAO,MAsG3C,CAAA"}
1
+ {"version":3,"file":"blog-comments.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-comments.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,kBAAmB,MAAM,KAAG,MAsGhE,CAAA"}
@@ -29,6 +29,6 @@ exports.BLOG_COMMENTS_RENDER_LIMIT = 200;
29
29
  * MUST mirror `buildBlogComments` in teleport-gui
30
30
  * `packages/renderer/src/utils/blog-comments.ts` (the canvas).
31
31
  */
32
- var generateBlogCommentsCode = function () { return "\nvar BLOG_COMMENTS_RENDER_LIMIT = ".concat(exports.BLOG_COMMENTS_RENDER_LIMIT, "\n\nvar BLOG_COMMENTS_SQL =\n 'WITH top AS (' +\n ' SELECT id FROM teleport_blog_comments' +\n \" WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'\" +\n ' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +\n ')' +\n ' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +\n ' FROM teleport_blog_comments c' +\n \" WHERE c.post_id = $1 AND c.status = 'approved'\" +\n ' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +\n ' ORDER BY c.created_at ASC, c.id ASC'\n\n// Every approved comment the page could show \u2014 a reply counts only while the\n// comment it answers is approved too.\nvar BLOG_COMMENTS_COUNT_SQL =\n 'SELECT COUNT(*) AS n FROM teleport_blog_comments c' +\n \" WHERE c.post_id = $1 AND c.status = 'approved'\" +\n ' AND (c.parent_id IS NULL OR EXISTS (' +\n ' SELECT 1 FROM teleport_blog_comments p' +\n \" WHERE p.id = c.parent_id AND p.status = 'approved'))\"\n\nfunction blogAuthorInitials(name) {\n if (typeof name !== 'string') return ''\n var words = name.trim().split(' ').filter(function(word) { return word !== '' })\n if (words.length === 0) return ''\n // Code points, not UTF-16 units: an initial may be an emoji or a letter\n // outside the BMP, and half of a surrogate pair renders as a box.\n var first = Array.from(words[0])[0] || ''\n var last = words.length > 1 ? Array.from(words[words.length - 1])[0] || '' : ''\n return (first + last).toUpperCase()\n}\n\nfunction buildBlogComment(row) {\n var name = typeof row.author_name === 'string' ? row.author_name.trim() : ''\n var authorName = name || 'Reader'\n return {\n id: String(row.id),\n authorName: authorName,\n authorInitials: blogAuthorInitials(authorName),\n content: typeof row.content === 'string' ? row.content : '',\n createdAt: normalizeTimestamp(row.created_at),\n isAuthorReply: coerceBoolean(row.is_author_reply, false),\n }\n}\n\nfunction isTopLevelComment(row) {\n return row.parent_id === null || row.parent_id === undefined || row.parent_id === ''\n}\n\nfunction buildBlogComments(rows) {\n var comments = []\n if (!Array.isArray(rows)) return comments\n var byId = {}\n for (var i = 0; i < rows.length; i++) {\n var row = rows[i]\n if (!row || row.id == null || !isTopLevelComment(row)) continue\n var comment = buildBlogComment(row)\n comment.replies = []\n byId[comment.id] = comment\n comments.push(comment)\n }\n for (var j = 0; j < rows.length; j++) {\n var reply = rows[j]\n if (!reply || reply.id == null || isTopLevelComment(reply)) continue\n var parent = byId[String(reply.parent_id)]\n if (parent) parent.replies.push(buildBlogComment(reply))\n }\n return comments\n}\n\n// { [postId]: { rows, count } } for a single-record (details) fetch; {} for\n// anything else, and on any failure \u2014 a blog whose comments table does not\n// exist yet renders its posts without comments rather than failing the page.\nasync function getBlogCommentsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length !== 1) return map\n var postId = records[0] && records[0].id\n if (postId === null || postId === undefined) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var listed = await client.query(BLOG_COMMENTS_SQL, [String(postId)])\n var counted = await client.query(BLOG_COMMENTS_COUNT_SQL, [String(postId)])\n // Through JSON, like every other row the fetcher hands the transform: the\n // driver returns timestamps as Date objects.\n var rows = listed && Array.isArray(listed.rows) ? JSON.parse(JSON.stringify(listed.rows)) : []\n var count = counted && counted.rows && counted.rows[0] ? Number(counted.rows[0].n) : NaN\n map[String(postId)] = { rows: rows, count: isNaN(count) ? rows.length : count }\n } catch (e) {\n // Leaves the post without comments.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n"); };
32
+ var generateBlogCommentsCode = function (commentsTable) { return "\nvar BLOG_COMMENTS_RENDER_LIMIT = ".concat(exports.BLOG_COMMENTS_RENDER_LIMIT, "\n\nvar BLOG_COMMENTS_SQL =\n 'WITH top AS (' +\n ' SELECT id FROM ").concat(commentsTable, "' +\n \" WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'\" +\n ' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +\n ')' +\n ' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +\n ' FROM ").concat(commentsTable, " c' +\n \" WHERE c.post_id = $1 AND c.status = 'approved'\" +\n ' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +\n ' ORDER BY c.created_at ASC, c.id ASC'\n\n// Every approved comment the page could show \u2014 a reply counts only while the\n// comment it answers is approved too.\nvar BLOG_COMMENTS_COUNT_SQL =\n 'SELECT COUNT(*) AS n FROM ").concat(commentsTable, " c' +\n \" WHERE c.post_id = $1 AND c.status = 'approved'\" +\n ' AND (c.parent_id IS NULL OR EXISTS (' +\n ' SELECT 1 FROM ").concat(commentsTable, " p' +\n \" WHERE p.id = c.parent_id AND p.status = 'approved'))\"\n\nfunction blogAuthorInitials(name) {\n if (typeof name !== 'string') return ''\n var words = name.trim().split(' ').filter(function(word) { return word !== '' })\n if (words.length === 0) return ''\n // Code points, not UTF-16 units: an initial may be an emoji or a letter\n // outside the BMP, and half of a surrogate pair renders as a box.\n var first = Array.from(words[0])[0] || ''\n var last = words.length > 1 ? Array.from(words[words.length - 1])[0] || '' : ''\n return (first + last).toUpperCase()\n}\n\nfunction buildBlogComment(row) {\n var name = typeof row.author_name === 'string' ? row.author_name.trim() : ''\n var authorName = name || 'Reader'\n return {\n id: String(row.id),\n authorName: authorName,\n authorInitials: blogAuthorInitials(authorName),\n content: typeof row.content === 'string' ? row.content : '',\n createdAt: normalizeTimestamp(row.created_at),\n isAuthorReply: coerceBoolean(row.is_author_reply, false),\n }\n}\n\nfunction isTopLevelComment(row) {\n return row.parent_id === null || row.parent_id === undefined || row.parent_id === ''\n}\n\nfunction buildBlogComments(rows) {\n var comments = []\n if (!Array.isArray(rows)) return comments\n var byId = {}\n for (var i = 0; i < rows.length; i++) {\n var row = rows[i]\n if (!row || row.id == null || !isTopLevelComment(row)) continue\n var comment = buildBlogComment(row)\n comment.replies = []\n byId[comment.id] = comment\n comments.push(comment)\n }\n for (var j = 0; j < rows.length; j++) {\n var reply = rows[j]\n if (!reply || reply.id == null || isTopLevelComment(reply)) continue\n var parent = byId[String(reply.parent_id)]\n if (parent) parent.replies.push(buildBlogComment(reply))\n }\n return comments\n}\n\n// { [postId]: { rows, count } } for a single-record (details) fetch; {} for\n// anything else, and on any failure \u2014 a blog whose comments table does not\n// exist yet renders its posts without comments rather than failing the page.\nasync function getBlogCommentsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length !== 1) return map\n var postId = records[0] && records[0].id\n if (postId === null || postId === undefined) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var listed = await client.query(BLOG_COMMENTS_SQL, [String(postId)])\n var counted = await client.query(BLOG_COMMENTS_COUNT_SQL, [String(postId)])\n // Through JSON, like every other row the fetcher hands the transform: the\n // driver returns timestamps as Date objects.\n var rows = listed && Array.isArray(listed.rows) ? JSON.parse(JSON.stringify(listed.rows)) : []\n var count = counted && counted.rows && counted.rows[0] ? Number(counted.rows[0].n) : NaN\n map[String(postId)] = { rows: rows, count: isNaN(count) ? rows.length : count }\n } catch (e) {\n // Leaves the post without comments.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n"); };
33
33
  exports.generateBlogCommentsCode = generateBlogCommentsCode;
34
34
  //# sourceMappingURL=blog-comments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-comments.js","sourceRoot":"","sources":["../../../src/transformations/blog-comments.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACU,QAAA,0BAA0B,GAAG,GAAG,CAAA;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACI,IAAM,wBAAwB,GAAG,cAAc,OAAA,6CACnB,kCAA0B,y/HAqG5D,EAtGqD,CAsGrD,CAAA;AAtGY,QAAA,wBAAwB,4BAsGpC"}
1
+ {"version":3,"file":"blog-comments.js","sourceRoot":"","sources":["../../../src/transformations/blog-comments.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACU,QAAA,0BAA0B,GAAG,GAAG,CAAA;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACI,IAAM,wBAAwB,GAAG,UAAC,aAAqB,IAAa,OAAA,6CACxC,kCAA0B,kFAIxC,aAAa,yRAKvB,aAAa,mYAQO,aAAa,4IAGxB,aAAa,2lGAiFhC,EAtG0E,CAsG1E,CAAA;AAtGY,QAAA,wBAAwB,4BAsGpC"}
@@ -1,10 +1,24 @@
1
1
  import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types';
2
+ import { ContentTables } from '@teleporthq/teleport-shared';
2
3
  /** What the blog-post transform bakes in at export time beyond the row itself. */
3
4
  export interface BlogPostTransformOptions {
5
+ /**
6
+ * The preset's tables (the Blog's by default): the posts table the related
7
+ * and adjacent posts are read from, the comments table a post page counts.
8
+ */
9
+ tables?: ContentTables.ContentTables;
4
10
  /** Category taxonomy — lives only in the UIDL, there is no DB table for it. */
5
11
  categories?: UIDLEcommerceCategory[];
6
12
  /** Content headings get ids and `#` links — see `heading-anchors.ts`. */
7
13
  headingAnchors?: boolean;
14
+ /**
15
+ * The post pages carry a Contents list: the content's headings get their ids
16
+ * (the `#` links only with `headingAnchors`) and a post page's post its
17
+ * `sections` — see `blogContentsSectionsOf`.
18
+ */
19
+ contents?: boolean;
20
+ /** Manual Order: Previous / Next walk the post's category as arranged — see `blog-adjacent-posts.ts`. */
21
+ arranged?: boolean;
8
22
  }
9
23
  /**
10
24
  * Generates JavaScript code for blog post data transformation.
@@ -1 +1 @@
1
- {"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAMvE,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACpC,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,aACpC,wBAAwB,KAChC,MAqQF,CAAA"}
1
+ {"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,aAAa,EAA0B,MAAM,6BAA6B,CAAA;AAMnF,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,aAAa,CAAA;IACpC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACpC,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,aACpC,wBAAwB,KAChC,MA0SF,CAAA"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateBlogPostTransformationCode = void 0;
4
+ var teleport_shared_1 = require("@teleporthq/teleport-shared");
4
5
  var category_taxonomy_1 = require("./category-taxonomy");
5
6
  var blog_adjacent_posts_1 = require("./blog-adjacent-posts");
6
7
  var blog_comments_1 = require("./blog-comments");
@@ -11,8 +12,10 @@ var heading_anchors_1 = require("./heading-anchors");
11
12
  * that UIDL components expect.
12
13
  */
13
14
  var generateBlogPostTransformationCode = function (options) {
15
+ var _a, _b;
14
16
  if (options === void 0) { options = {}; }
15
- return "\n// ============================================================\n// Blog Post Transformation\n// ============================================================\n".concat((0, category_taxonomy_1.generateCategoryTaxonomyCode)('BLOG_CATEGORIES_BY_ID', options.categories), "\n").concat((0, blog_comments_1.generateBlogCommentsCode)(), "\n").concat((0, blog_adjacent_posts_1.generateAdjacentPostsCode)(), "\n").concat((0, heading_anchors_1.generateHeadingAnchorsCode)(), "\nvar BLOG_HEADING_ANCHORS = ").concat(options.headingAnchors === true, "\n\nfunction buildBlogPost(record, options) {\n if (!record || typeof record !== 'object') return record\n options = options || {}\n var currentLang = options.currentLanguage || null\n var mainLang = options.mainLanguage || null\n var assetMap = options.assetMap || {}\n\n var id = record.id !== undefined && record.id !== null ? record.id : null\n\n // i18n-resolved text fields\n var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''\n var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''\n var content = resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''\n if (BLOG_HEADING_ANCHORS) content = addHeadingAnchors(content)\n var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''\n var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null\n\n // Assigned category ids -> resolved {id,name,slug} objects for the post-details\n // BREADCRUMBS (and anything else mapping over them). The taxonomy itself is not\n // in the DB (categories are authored in the GUI and live only in\n // blogSettings.categories); resolved against BLOG_CATEGORIES_BY_ID, baked in at\n // export time. Unknown/stale ids are silently dropped. `category` above stays\n // the denormalized PRIMARY name.\n var categories = resolveAssignedCategories(\n record.category_ids,\n BLOG_CATEGORIES_BY_ID,\n currentLang,\n mainLang,\n category\n )\n var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null\n var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null\n var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null\n var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null\n\n // Status\n var status = record.status || 'draft'\n\n // Asset fields - resolve IDs to URLs\n var rawFeaturedImage = pickFirst(record.featured_image_url, record.featuredImageUrl)\n var TELEPORT_DEFAULT_FEATURED_IMG = 'https://play.teleporthq.io/static/svg/default-img.svg'\n var featuredImageUrl = resolveAssetUrl(rawFeaturedImage, assetMap)\n if (\n !featuredImageUrl ||\n rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||\n featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG\n ) {\n featuredImageUrl =\n 'data:image/svg+xml;charset=utf-8,' +\n encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"240\" viewBox=\"0 0 400 240\"><rect fill=\"#e5e7eb\" width=\"400\" height=\"240\"/><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" fill=\"#9ca3af\" font-family=\"system-ui,sans-serif\" font-size=\"13\">No image</text></svg>'\n )\n }\n\n var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)\n var authorAvatarUrl = resolveAssetUrl(rawAuthorAvatar, assetMap)\n\n // Simple pass-through fields\n var authorName = pickFirst(record.author_name, record.authorName)\n var authorInitials = blogAuthorInitials(authorName)\n var authorEmail = pickFirst(record.author_email, record.authorEmail)\n var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)\n\n // Tags - i18n-resolved then parsed as JSON array\n var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)\n var tags = parseJsonArray(rawTags)\n\n // Gallery images - parse JSON array then resolve each asset URL\n var rawGalleryImages = pickFirst(record.gallery_images, record.galleryImages)\n var parsedGalleryImages = parseJsonArray(rawGalleryImages)\n var galleryImages = resolveAssetUrls(parsedGalleryImages, assetMap)\n\n // Computed: allImages = [featuredImageUrl, ...galleryImages] (skip null featured)\n var allImages = []\n if (featuredImageUrl) allImages.push(featuredImageUrl)\n allImages = allImages.concat(galleryImages)\n\n // Boolean fields\n var isFeatured = coerceBoolean(pickFirst(record.is_featured, record.isFeatured), false)\n var allowComments = record.allow_comments !== undefined\n ? coerceBoolean(record.allow_comments, true)\n : record.allowComments !== undefined\n ? coerceBoolean(record.allowComments, true)\n : true\n\n // Per-post SEO overrides. The transform is the single normalization point \u2014\n // rows can also be written by the raw admin CRUD form, so whitespace and\n // unknown redirect types are neutralized here. Absent columns (a table\n // provisioned before the feature) read as undefined and normalize to null.\n // robotsContent is null \u2014 never '' or false \u2014 for unset rows, so the\n // page-level fallback (the `??` in the generated Head) can take over.\n var noIndex = coerceBoolean(pickFirst(record.no_index, record.noIndex), false)\n var canonicalUrl = normalizeSeoUrlField(pickFirst(record.canonical_url, record.canonicalUrl))\n var redirectUrl = normalizeSeoUrlField(pickFirst(record.redirect_url, record.redirectUrl))\n var rawRedirectType = pickFirst(record.redirect_type, record.redirectType)\n var redirectType =\n redirectUrl && (rawRedirectType === '301' || rawRedirectType === '302')\n ? rawRedirectType\n : null\n var robotsContent = noIndex ? 'noindex' : null\n\n // The author's related-post picks. `relatedPosts` carries the TRANSFORMED\n // rows, not ids, so the details page's related-posts rail can map over it and\n // draw an article card per entry; `relatedPostIds` keeps the raw selection.\n // The rows arrive from ONE batched query (getRelatedPostsMap) that\n // transformRecords runs, keyed by id in options.relatedPostsById.\n //\n // MUST mirror buildRelatedBlogPosts in packages/renderer/src/utils/blog-posts.ts\n // (the canvas-preview SSOT): the author's order, UNPUBLISHED posts dropped,\n // self dropped, and the nested build deliberately WITHOUT the map \u2014 two posts\n // pointing at each other is ordinary and would otherwise recurse forever.\n var relatedPostIds = parseRelatedIds(record.related_post_ids)\n var relatedPostsById = options.relatedPostsById\n var relatedPosts = []\n // The nested builds of the related and adjacent posts get NO maps: each is a\n // card, not a page, and two posts that point at each other would recurse.\n var relatedNestedOptions = {\n assetMap: assetMap,\n currentLanguage: currentLang,\n mainLanguage: mainLang,\n }\n if (relatedPostsById && relatedPostIds.length > 0) {\n for (var rp = 0; rp < relatedPostIds.length; rp++) {\n var relatedId = relatedPostIds[rp]\n if (id != null && relatedId === String(id)) continue\n var relatedRecord = relatedPostsById[relatedId]\n if (!relatedRecord) continue\n // A draft must not reach the storefront through another post's rail.\n // Missing status reads as 'draft', the same default as above.\n if ((relatedRecord.status || 'draft') !== 'published') continue\n relatedPosts.push(buildBlogPost(relatedRecord, relatedNestedOptions))\n }\n }\n\n // The previous (older) and next (newer) published post, as 0-or-1-item\n // lists: the post navigation maps over them like the related rail does, so an\n // end of the blog simply renders no card on that side.\n var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null\n var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []\n var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []\n\n // The approved comments \u2014 the newest top-level ones, each with its replies \u2014\n // present only on a details fetch; see blog-comments.ts.\n var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null\n var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []\n var commentsCount = commentsEntry ? commentsEntry.count : 0\n\n // Timestamps\n var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)\n var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null\n\n var rawCreatedAt = pickFirst(record.created_at, record.createdAt, record.created)\n var createdAt = normalizeTimestamp(rawCreatedAt)\n\n var rawUpdatedAt = pickFirst(record.updated_at, record.updatedAt, record.updated)\n var updatedAt = rawUpdatedAt != null ? normalizeTimestamp(rawUpdatedAt) : createdAt\n\n // Computed: created = Unix seconds from createdAt milliseconds\n var created = Math.floor(createdAt / 1000)\n\n return {\n id: id,\n title: title,\n slug: slug,\n content: content,\n excerpt: excerpt,\n status: status,\n category: category,\n categories: categories,\n tags: tags,\n relatedPostIds: relatedPostIds,\n relatedPosts: relatedPosts,\n featuredImageUrl: featuredImageUrl,\n featuredImageAlt: featuredImageAlt,\n galleryImages: galleryImages,\n allImages: allImages,\n authorName: authorName,\n authorEmail: authorEmail,\n author_name: authorName,\n author_email: authorEmail,\n authorAvatarUrl: authorAvatarUrl,\n authorBio: authorBio,\n authorInitials: authorInitials,\n metaTitle: metaTitle,\n metaDescription: metaDescription,\n readingTimeMinutes: readingTimeMinutes,\n isFeatured: isFeatured,\n allowComments: allowComments,\n comments: comments,\n commentsCount: commentsCount,\n previousPosts: previousPosts,\n nextPosts: nextPosts,\n noIndex: noIndex,\n canonicalUrl: canonicalUrl,\n redirectUrl: redirectUrl,\n redirectType: redirectType,\n robotsContent: robotsContent,\n publishedAt: publishedAt,\n createdAt: createdAt,\n updatedAt: updatedAt,\n created: created,\n }\n}\n\nfunction transformBlogPosts(records, options) {\n if (!Array.isArray(records)) return []\n return records.map(function(record) { return buildBlogPost(record, options) })\n}\n\n// Batched fetch of the post rows a set of posts reference as \"related\", keyed by\n// id. Best-effort in the same way as the product side: a table provisioned\n// before the column yields no ids (the read is of an absent property, not of a\n// column \u2014 the SQL never names it), the map stays empty, and the rail stays\n// hidden behind its is_not_empty gate.\nasync function getRelatedPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length === 0) return map\n\n var wantedIds = []\n for (var i = 0; i < records.length; i++) {\n var ids = parseRelatedIds(records[i] && records[i].related_post_ids)\n for (var j = 0; j < ids.length; j++) {\n if (wantedIds.indexOf(ids[j]) === -1) wantedIds.push(ids[j])\n }\n }\n if (wantedIds.length === 0) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var result = await client.query('SELECT * FROM teleport_blog_posts WHERE id = ANY($1)', [\n wantedIds,\n ])\n if (result && result.rows) {\n for (var r = 0; r < result.rows.length; r++) {\n var row = result.rows[r]\n if (row && row.id != null) map[String(row.id)] = row\n }\n }\n } catch (e) {\n // Leaves relatedPosts empty rather than failing the page.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n");
17
+ var tables = (_a = options.tables) !== null && _a !== void 0 ? _a : teleport_shared_1.ContentTables.contentTablesByKey('blog');
18
+ return "\n// ============================================================\n// Blog Post Transformation\n// ============================================================\n".concat((0, category_taxonomy_1.generateCategoryTaxonomyCode)('BLOG_CATEGORIES_BY_ID', options.categories), "\n").concat((0, blog_comments_1.generateBlogCommentsCode)((_b = tables.comments) !== null && _b !== void 0 ? _b : "".concat(tables.posts, "_comments")), "\n").concat((0, blog_adjacent_posts_1.generateAdjacentPostsCode)(tables.posts, options.arranged === true), "\n").concat((0, heading_anchors_1.generateHeadingAnchorsCode)(), "\n").concat(teleport_shared_1.RichTextContentCodegen.generateLegacyRichTextNormalizerCode(), "\nvar BLOG_HEADING_ANCHORS = ").concat(options.headingAnchors === true, "\nvar BLOG_CONTENTS = ").concat(options.contents === true, "\nvar BLOG_PICTURE_STAND_IN = ").concat(tables.pictureStandIn !== false, "\n\n// A post's reading time from its body, for one that stores none: its words at\n// about 200 a minute, at least one \u2014 what the editor's post form computes. A\n// card or post page otherwise printed \"min read\" with no number before it.\n// MUST mirror readingTimeOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.\nfunction blogReadingTimeOf(html) {\n var text = String(html || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()\n return text ? Math.max(1, Math.ceil(text.split(' ').length / 200)) : null\n}\n\nfunction buildBlogPost(record, options) {\n if (!record || typeof record !== 'object') return record\n options = options || {}\n var currentLang = options.currentLanguage || null\n var mainLang = options.mainLanguage || null\n var assetMap = options.assetMap || {}\n\n var id = record.id !== undefined && record.id !== null ? record.id : null\n\n // i18n-resolved text fields\n var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''\n var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''\n // A body stored before the rich-text contract (the studio's flat list lines,\n // the admin panel's no-break spaces) reads as stored HTML from here on.\n var content = normalizeLegacyRichTextHtml(\n resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''\n )\n // Counted before the heading anchors add their marks.\n var bodyReadingTime = blogReadingTimeOf(content)\n // The Contents list links to the headings, so they get their ids for it too\n // \u2014 the # links only with the heading anchors on. Its sections ride on a\n // post page's own fetch only (options.details): a listing draws no list, and\n // every card of it would otherwise carry one.\n var contentsHeadings = BLOG_CONTENTS && options.details === true ? [] : null\n if (BLOG_HEADING_ANCHORS || BLOG_CONTENTS) {\n content = addHeadingAnchors(content, { links: BLOG_HEADING_ANCHORS, sections: contentsHeadings })\n }\n var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''\n var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null\n\n // Assigned category ids -> resolved {id,name,slug} objects for the post-details\n // BREADCRUMBS (and anything else mapping over them). The taxonomy itself is not\n // in the DB (categories are authored in the GUI and live only in\n // blogSettings.categories); resolved against BLOG_CATEGORIES_BY_ID, baked in at\n // export time. Unknown/stale ids are silently dropped. `category` above stays\n // the denormalized PRIMARY name.\n var categories = resolveAssignedCategories(\n record.category_ids,\n BLOG_CATEGORIES_BY_ID,\n currentLang,\n mainLang,\n category\n )\n var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null\n var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null\n var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null\n var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null\n\n // Status\n var status = record.status || 'draft'\n\n // Asset fields - resolve IDs to URLs\n var rawFeaturedImage = pickFirst(record.featured_image_url, record.featuredImageUrl)\n var TELEPORT_DEFAULT_FEATURED_IMG = 'https://play.teleporthq.io/static/svg/default-img.svg'\n var featuredImageUrl = resolveAssetUrl(rawFeaturedImage, assetMap)\n if (\n !featuredImageUrl ||\n rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||\n featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG\n ) {\n // No picture of its own: the grey \"No image\" stand-in, or none at all for a\n // preset whose posts are text (ContentTables.pictureStandIn) \u2014 its card and\n // post page show the picture only when there is one.\n featuredImageUrl = BLOG_PICTURE_STAND_IN\n ? 'data:image/svg+xml;charset=utf-8,' +\n encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"240\" viewBox=\"0 0 400 240\"><rect fill=\"#e5e7eb\" width=\"400\" height=\"240\"/><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" fill=\"#9ca3af\" font-family=\"system-ui,sans-serif\" font-size=\"13\">No image</text></svg>'\n )\n : null\n }\n\n var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)\n var authorAvatarUrl = resolveAssetUrl(rawAuthorAvatar, assetMap)\n\n // Simple pass-through fields\n var authorName = pickFirst(record.author_name, record.authorName)\n var authorInitials = blogAuthorInitials(authorName)\n // The author's email never leaves the server: a visitor's API read hides it\n // (browser-row-policy.ts), and the page data a post or listing page ships\n // is built here, so it is not part of the post a page sees either.\n var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)\n if (!(readingTimeMinutes > 0)) readingTimeMinutes = bodyReadingTime\n\n // Tags - i18n-resolved then parsed as JSON array\n var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)\n var tags = parseJsonArray(rawTags)\n\n // Gallery images - parse JSON array then resolve each asset URL\n var rawGalleryImages = pickFirst(record.gallery_images, record.galleryImages)\n var parsedGalleryImages = parseJsonArray(rawGalleryImages)\n var galleryImages = resolveAssetUrls(parsedGalleryImages, assetMap)\n\n // Computed: allImages = [featuredImageUrl, ...galleryImages] (skip null featured)\n var allImages = []\n if (featuredImageUrl) allImages.push(featuredImageUrl)\n allImages = allImages.concat(galleryImages)\n\n // Boolean fields\n var isFeatured = coerceBoolean(pickFirst(record.is_featured, record.isFeatured), false)\n var allowComments = record.allow_comments !== undefined\n ? coerceBoolean(record.allow_comments, true)\n : record.allowComments !== undefined\n ? coerceBoolean(record.allowComments, true)\n : true\n\n // Per-post SEO overrides. The transform is the single normalization point \u2014\n // rows can also be written by the raw admin CRUD form, so whitespace and\n // unknown redirect types are neutralized here. Absent columns (a table\n // provisioned before the feature) read as undefined and normalize to null.\n // robotsContent is null \u2014 never '' or false \u2014 for unset rows, so the\n // page-level fallback (the `??` in the generated Head) can take over.\n var noIndex = coerceBoolean(pickFirst(record.no_index, record.noIndex), false)\n var canonicalUrl = normalizeSeoUrlField(pickFirst(record.canonical_url, record.canonicalUrl))\n var redirectUrl = normalizeSeoUrlField(pickFirst(record.redirect_url, record.redirectUrl))\n var rawRedirectType = pickFirst(record.redirect_type, record.redirectType)\n var redirectType =\n redirectUrl && (rawRedirectType === '301' || rawRedirectType === '302')\n ? rawRedirectType\n : null\n var robotsContent = noIndex ? 'noindex' : null\n\n // The author's related-post picks. `relatedPosts` carries the TRANSFORMED\n // rows, not ids, so the details page's related-posts rail can map over it and\n // draw an article card per entry; `relatedPostIds` keeps the raw selection.\n // The rows arrive from ONE batched query (getRelatedPostsMap) that\n // transformRecords runs, keyed by id in options.relatedPostsById.\n //\n // MUST mirror buildRelatedBlogPosts in packages/renderer/src/utils/blog-posts.ts\n // (the canvas-preview SSOT): the author's order, UNPUBLISHED posts dropped,\n // self dropped, and the nested build deliberately WITHOUT the map \u2014 two posts\n // pointing at each other is ordinary and would otherwise recurse forever.\n var relatedPostIds = parseRelatedIds(record.related_post_ids)\n var relatedPostsById = options.relatedPostsById\n var relatedPosts = []\n // The nested builds of the related and adjacent posts get NO maps: each is a\n // card, not a page, and two posts that point at each other would recurse.\n var relatedNestedOptions = {\n assetMap: assetMap,\n currentLanguage: currentLang,\n mainLanguage: mainLang,\n }\n if (relatedPostsById && relatedPostIds.length > 0) {\n for (var rp = 0; rp < relatedPostIds.length; rp++) {\n var relatedId = relatedPostIds[rp]\n if (id != null && relatedId === String(id)) continue\n var relatedRecord = relatedPostsById[relatedId]\n if (!relatedRecord) continue\n // A draft must not reach the storefront through another post's rail.\n // Missing status reads as 'draft', the same default as above.\n if ((relatedRecord.status || 'draft') !== 'published') continue\n relatedPosts.push(buildBlogPost(relatedRecord, relatedNestedOptions))\n }\n }\n\n // The previous (older) and next (newer) published post, as 0-or-1-item\n // lists: the post navigation maps over them like the related rail does, so an\n // end of the blog simply renders no card on that side.\n var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null\n var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []\n var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []\n\n // The approved comments \u2014 the newest top-level ones, each with its replies \u2014\n // present only on a details fetch; see blog-comments.ts.\n var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null\n var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []\n var commentsCount = commentsEntry ? commentsEntry.count : 0\n\n // Timestamps\n var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)\n var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null\n\n var rawCreatedAt = pickFirst(record.created_at, record.createdAt, record.created)\n var createdAt = normalizeTimestamp(rawCreatedAt)\n\n var rawUpdatedAt = pickFirst(record.updated_at, record.updatedAt, record.updated)\n var updatedAt = rawUpdatedAt != null ? normalizeTimestamp(rawUpdatedAt) : createdAt\n\n // Computed: created = Unix seconds from createdAt milliseconds\n var created = Math.floor(createdAt / 1000)\n\n return {\n id: id,\n title: title,\n slug: slug,\n content: content,\n excerpt: excerpt,\n status: status,\n category: category,\n categories: categories,\n tags: tags,\n relatedPostIds: relatedPostIds,\n relatedPosts: relatedPosts,\n featuredImageUrl: featuredImageUrl,\n featuredImageAlt: featuredImageAlt,\n galleryImages: galleryImages,\n allImages: allImages,\n authorName: authorName,\n author_name: authorName,\n authorAvatarUrl: authorAvatarUrl,\n authorBio: authorBio,\n authorInitials: authorInitials,\n metaTitle: metaTitle,\n metaDescription: metaDescription,\n readingTimeMinutes: readingTimeMinutes,\n // The post's sections, what its page's Contents list links to \u2014 see\n // blogContentsSectionsOf; empty off a post page or under three sections.\n sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],\n isFeatured: isFeatured,\n allowComments: allowComments,\n comments: comments,\n commentsCount: commentsCount,\n previousPosts: previousPosts,\n nextPosts: nextPosts,\n noIndex: noIndex,\n canonicalUrl: canonicalUrl,\n redirectUrl: redirectUrl,\n redirectType: redirectType,\n robotsContent: robotsContent,\n publishedAt: publishedAt,\n createdAt: createdAt,\n updatedAt: updatedAt,\n created: created,\n // The post's place among its primary category's posts (Manual Order), or\n // null: what the category pages order by.\n sortOrder: safeNumber(pickFirst(record.sort_order, record.sortOrder), null),\n }\n}\n\nfunction transformBlogPosts(records, options) {\n if (!Array.isArray(records)) return []\n return records.map(function(record) { return buildBlogPost(record, options) })\n}\n\n// Batched fetch of the post rows a set of posts reference as \"related\", keyed by\n// id. Best-effort in the same way as the product side: a table provisioned\n// before the column yields no ids (the read is of an absent property, not of a\n// column \u2014 the SQL never names it), the map stays empty, and the rail stays\n// hidden behind its is_not_empty gate.\nasync function getRelatedPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length === 0) return map\n\n var wantedIds = []\n for (var i = 0; i < records.length; i++) {\n var ids = parseRelatedIds(records[i] && records[i].related_post_ids)\n for (var j = 0; j < ids.length; j++) {\n if (wantedIds.indexOf(ids[j]) === -1) wantedIds.push(ids[j])\n }\n }\n if (wantedIds.length === 0) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var result = await client.query('SELECT * FROM ").concat(tables.posts, " WHERE id = ANY($1)', [\n wantedIds,\n ])\n if (result && result.rows) {\n for (var r = 0; r < result.rows.length; r++) {\n var row = result.rows[r]\n if (row && row.id != null) map[String(row.id)] = row\n }\n }\n } catch (e) {\n // Leaves relatedPosts empty rather than failing the page.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n");
16
19
  };
17
20
  exports.generateBlogPostTransformationCode = generateBlogPostTransformationCode;
18
21
  //# sourceMappingURL=blog-post.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":";;;AACA,yDAAkE;AAClE,6DAAiE;AACjE,iDAA0D;AAC1D,qDAA8D;AAU9D;;;;GAIG;AACI,IAAM,kCAAkC,GAAG,UAChD,OAAsC;IAAtC,wBAAA,EAAA,YAAsC;IAEtC,OAAO,2KAIP,IAAA,gDAA4B,EAAC,uBAAuB,EAAE,OAAO,CAAC,UAAU,CAAC,eACzE,IAAA,wCAAwB,GAAE,eAC1B,IAAA,+CAAyB,GAAE,eAC3B,IAAA,4CAA0B,GAAE,0CACD,OAAO,CAAC,cAAc,KAAK,IAAI,8xWA2P3D,CAAA;AACD,CAAC,CAAA;AAvQY,QAAA,kCAAkC,sCAuQ9C"}
1
+ {"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":";;;AACA,+DAAmF;AACnF,yDAAkE;AAClE,6DAAiE;AACjE,iDAA0D;AAC1D,qDAA8D;AAuB9D;;;;GAIG;AACI,IAAM,kCAAkC,GAAG,UAChD,OAAsC;;IAAtC,wBAAA,EAAA,YAAsC;IAEtC,IAAM,MAAM,GAAG,MAAA,OAAO,CAAC,MAAM,mCAAI,+BAAa,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACzE,OAAO,2KAIP,IAAA,gDAA4B,EAAC,uBAAuB,EAAE,OAAO,CAAC,UAAU,CAAC,eACzE,IAAA,wCAAwB,EAAC,MAAA,MAAM,CAAC,QAAQ,mCAAI,UAAG,MAAM,CAAC,KAAK,cAAW,CAAC,eACvE,IAAA,+CAAyB,EAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,eAClE,IAAA,4CAA0B,GAAE,eAC5B,wCAAsB,CAAC,oCAAoC,EAAE,0CAClC,OAAO,CAAC,cAAc,KAAK,IAAI,mCACtC,OAAO,CAAC,QAAQ,KAAK,IAAI,2CACjB,MAAM,CAAC,cAAc,KAAK,KAAK,49ZA0QR,MAAM,CAAC,KAAK,6cAkBhE,CAAA;AACD,CAAC,CAAA;AA5SY,QAAA,kCAAkC,sCA4S9C"}
@@ -8,15 +8,39 @@
8
8
  */
9
9
  export declare const BLOG_HEADING_ANCHOR_CLASS = "tq-heading-anchor";
10
10
  /**
11
- * Generates `addHeadingAnchors(html)`: every `<h2>`–`<h6>` of a post's content
12
- * gets an `id` (kept when it already has one, otherwise a slug of its text,
13
- * unique within the post) and a trailing `#` link to itself, so a section can
14
- * be linked to (`/blog/post#setup`) and the link works on the very first load —
15
- * the ids are in the server-rendered HTML.
11
+ * The class of the link a post's content heading's own text becomes.
16
12
  *
17
- * The link repeats the heading a screen reader already announces, so it is
18
- * hidden from assistive technology and left out of the tab order; the heading
19
- * stays the landmark.
13
+ * ⛔ PAIRED with `BLOG_HEADING_LINK_CLASS` in teleport-gui
14
+ * `features/blog/utils/elements/heading-anchor-styles.ts`, whose stylesheet
15
+ * keeps the heading looking like a heading rather than like a link.
16
+ */
17
+ export declare const BLOG_HEADING_LINK_CLASS = "tq-heading-link";
18
+ /**
19
+ * The fewest sections a post's Contents list is drawn for: a shorter post is
20
+ * taken in at a glance.
21
+ *
22
+ * ⛔ PAIRED with `CONTENTS_MIN_SECTIONS` in teleport-gui
23
+ * `packages/renderer/src/utils/blog-posts.ts`, the canvas's copy.
24
+ */
25
+ export declare const CONTENTS_MIN_SECTIONS = 3;
26
+ /**
27
+ * Generates `addHeadingAnchors(html, options)`: every `<h2>`–`<h6>` of a post's
28
+ * content gets an `id` (kept when it already has one, otherwise a slug of its
29
+ * text, unique within the post), its text becomes a link to itself and a `#`
30
+ * follows it, shown on hover, so a section can be linked to
31
+ * (`/blog/post#setup`) by clicking its heading, and the link works on the very
32
+ * first load — the ids are in the server-rendered HTML. A heading that holds a
33
+ * link of its own keeps its text as it is (a link cannot hold another) and
34
+ * links through the `#` alone.
35
+ *
36
+ * `options.links: false` gives the headings their ids without the links —
37
+ * what a post page's Contents list needs when the heading anchors are off.
38
+ * `options.sections`, an array, collects every heading as `{ level, id, text }`
39
+ * in reading order, and `blogContentsSectionsOf` turns those into the list.
40
+ *
41
+ * The heading's text is the link a screen reader announces and the keyboard
42
+ * reaches; the `#` repeats it, so it is hidden from assistive technology and
43
+ * left out of the tab order.
20
44
  *
21
45
  * Idempotent: a heading that already carries the link is left alone. `h1` is
22
46
  * the post title's level and is never touched.
@@ -1 +1 @@
1
- {"version":3,"file":"heading-anchors.d.ts","sourceRoot":"","sources":["../../../src/transformations/heading-anchors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,sBAAsB,CAAA;AAE5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,QAAO,MA4D7C,CAAA"}
1
+ {"version":3,"file":"heading-anchors.d.ts","sourceRoot":"","sources":["../../../src/transformations/heading-anchors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,sBAAsB,CAAA;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,oBAAoB,CAAA;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,0BAA0B,QAAO,MAiG7C,CAAA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateHeadingAnchorsCode = exports.BLOG_HEADING_ANCHOR_CLASS = void 0;
3
+ exports.generateHeadingAnchorsCode = exports.CONTENTS_MIN_SECTIONS = exports.BLOG_HEADING_LINK_CLASS = exports.BLOG_HEADING_ANCHOR_CLASS = void 0;
4
4
  /**
5
5
  * The class of the `#` link added to a post's content headings.
6
6
  *
@@ -11,19 +11,43 @@ exports.generateHeadingAnchorsCode = exports.BLOG_HEADING_ANCHOR_CLASS = void 0;
11
11
  */
12
12
  exports.BLOG_HEADING_ANCHOR_CLASS = 'tq-heading-anchor';
13
13
  /**
14
- * Generates `addHeadingAnchors(html)`: every `<h2>`–`<h6>` of a post's content
15
- * gets an `id` (kept when it already has one, otherwise a slug of its text,
16
- * unique within the post) and a trailing `#` link to itself, so a section can
17
- * be linked to (`/blog/post#setup`) and the link works on the very first load —
18
- * the ids are in the server-rendered HTML.
14
+ * The class of the link a post's content heading's own text becomes.
19
15
  *
20
- * The link repeats the heading a screen reader already announces, so it is
21
- * hidden from assistive technology and left out of the tab order; the heading
22
- * stays the landmark.
16
+ * ⛔ PAIRED with `BLOG_HEADING_LINK_CLASS` in teleport-gui
17
+ * `features/blog/utils/elements/heading-anchor-styles.ts`, whose stylesheet
18
+ * keeps the heading looking like a heading rather than like a link.
19
+ */
20
+ exports.BLOG_HEADING_LINK_CLASS = 'tq-heading-link';
21
+ /**
22
+ * The fewest sections a post's Contents list is drawn for: a shorter post is
23
+ * taken in at a glance.
24
+ *
25
+ * ⛔ PAIRED with `CONTENTS_MIN_SECTIONS` in teleport-gui
26
+ * `packages/renderer/src/utils/blog-posts.ts`, the canvas's copy.
27
+ */
28
+ exports.CONTENTS_MIN_SECTIONS = 3;
29
+ /**
30
+ * Generates `addHeadingAnchors(html, options)`: every `<h2>`–`<h6>` of a post's
31
+ * content gets an `id` (kept when it already has one, otherwise a slug of its
32
+ * text, unique within the post), its text becomes a link to itself and a `#`
33
+ * follows it, shown on hover, so a section can be linked to
34
+ * (`/blog/post#setup`) by clicking its heading, and the link works on the very
35
+ * first load — the ids are in the server-rendered HTML. A heading that holds a
36
+ * link of its own keeps its text as it is (a link cannot hold another) and
37
+ * links through the `#` alone.
38
+ *
39
+ * `options.links: false` gives the headings their ids without the links —
40
+ * what a post page's Contents list needs when the heading anchors are off.
41
+ * `options.sections`, an array, collects every heading as `{ level, id, text }`
42
+ * in reading order, and `blogContentsSectionsOf` turns those into the list.
43
+ *
44
+ * The heading's text is the link a screen reader announces and the keyboard
45
+ * reaches; the `#` repeats it, so it is hidden from assistive technology and
46
+ * left out of the tab order.
23
47
  *
24
48
  * Idempotent: a heading that already carries the link is left alone. `h1` is
25
49
  * the post title's level and is never touched.
26
50
  */
27
- var generateHeadingAnchorsCode = function () { return "\nvar BLOG_HEADING_ANCHOR_CLASS = '".concat(exports.BLOG_HEADING_ANCHOR_CLASS, "'\nvar BLOG_HEADING_ID_PATTERN = /\\sid\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/i\n\nfunction escapeHeadingAttribute(value) {\n return String(value)\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n}\n\nfunction blogHeadingSlug(text, used) {\n var base = String(text || '')\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n if (!base) base = 'section'\n var slug = base\n var suffix = 2\n while (used[slug]) {\n slug = base + '-' + suffix\n suffix++\n }\n used[slug] = true\n return slug\n}\n\nfunction addHeadingAnchors(html) {\n if (typeof html !== 'string' || !/<h[2-6][\\s>]/i.test(html)) return html\n // Every id the content already uses is taken, so a generated one never\n // duplicates it.\n var used = {}\n var existingIds = new RegExp(BLOG_HEADING_ID_PATTERN.source, 'gi')\n var existing\n while ((existing = existingIds.exec(html)) !== null) {\n used[decodeHtmlEntities(existing[2] || existing[3] || '')] = true\n }\n\n return html.replace(/<h([2-6])(\\s[^>]*)?>([\\s\\S]*?)<\\/h\\1>/gi, function(match, level, rawAttrs, inner) {\n if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) return match\n var text = stripHtmlTags(inner)\n if (!text) return match\n var attrs = rawAttrs || ''\n var idMatch = attrs.match(BLOG_HEADING_ID_PATTERN)\n var id = idMatch ? decodeHtmlEntities(idMatch[2] || idMatch[3] || '') : ''\n if (!id) {\n id = blogHeadingSlug(text, used)\n attrs = ' id=\"' + escapeHeadingAttribute(id) + '\"' + attrs\n }\n return (\n '<h' + level + attrs + '>' + inner +\n '<a class=\"' + BLOG_HEADING_ANCHOR_CLASS + '\" href=\"#' + escapeHeadingAttribute(id) +\n '\" aria-hidden=\"true\" tabindex=\"-1\">#</a>' +\n '</h' + level + '>'\n )\n })\n}\n"); };
51
+ var generateHeadingAnchorsCode = function () { return "\nvar BLOG_HEADING_ANCHOR_CLASS = '".concat(exports.BLOG_HEADING_ANCHOR_CLASS, "'\nvar BLOG_HEADING_LINK_CLASS = '").concat(exports.BLOG_HEADING_LINK_CLASS, "'\nvar BLOG_HEADING_ID_PATTERN = /\\sid\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/i\nvar BLOG_HEADING_ANCHOR_LINK_PATTERN = /<a\\s[^>]*class=\"").concat(exports.BLOG_HEADING_ANCHOR_CLASS, "\"[^>]*>[\\s\\S]*?<\\/a>/gi\nvar BLOG_CONTENTS_MIN_SECTIONS = ").concat(exports.CONTENTS_MIN_SECTIONS, "\n\nfunction escapeHeadingAttribute(value) {\n return String(value)\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n}\n\nfunction blogHeadingSlug(text, used) {\n var base = String(text || '')\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n if (!base) base = 'section'\n var slug = base\n var suffix = 2\n while (used[slug]) {\n slug = base + '-' + suffix\n suffix++\n }\n used[slug] = true\n return slug\n}\n\nfunction addHeadingAnchors(html, options) {\n options = options || {}\n var withLinks = options.links !== false\n var sections = Array.isArray(options.sections) ? options.sections : null\n if (typeof html !== 'string' || !/<h[2-6][\\s>]/i.test(html)) return html\n // Every id the content already uses is taken, so a generated one never\n // duplicates it. No prototype: a heading titled \"Constructor\" is free to be\n // #constructor.\n var used = Object.create(null)\n var existingIds = new RegExp(BLOG_HEADING_ID_PATTERN.source, 'gi')\n var existing\n while ((existing = existingIds.exec(html)) !== null) {\n used[decodeHtmlEntities(existing[2] || existing[3] || '')] = true\n }\n\n return html.replace(/<h([2-6])(\\s[^>]*)?>([\\s\\S]*?)<\\/h\\1>/gi, function(match, level, rawAttrs, inner) {\n var attrs = rawAttrs || ''\n var idMatch = attrs.match(BLOG_HEADING_ID_PATTERN)\n var id = idMatch ? decodeHtmlEntities(idMatch[2] || idMatch[3] || '') : ''\n if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) {\n if (sections && id) {\n var anchoredText = stripHtmlTags(inner.replace(BLOG_HEADING_ANCHOR_LINK_PATTERN, ''))\n if (anchoredText) sections.push({ level: Number(level), id: id, text: anchoredText })\n }\n return match\n }\n var text = stripHtmlTags(inner)\n if (!text) return match\n if (!id) {\n id = blogHeadingSlug(text, used)\n attrs = ' id=\"' + escapeHeadingAttribute(id) + '\"' + attrs\n }\n if (sections) sections.push({ level: Number(level), id: id, text: text })\n if (!withLinks) return '<h' + level + attrs + '>' + inner + '</h' + level + '>'\n var href = '#' + escapeHeadingAttribute(id)\n var ownText = /<a[\\s>]/i.test(inner)\n ? inner\n : '<a class=\"' + BLOG_HEADING_LINK_CLASS + '\" href=\"' + href + '\">' + inner + '</a>'\n return (\n '<h' + level + attrs + '>' + ownText +\n '<a class=\"' + BLOG_HEADING_ANCHOR_CLASS + '\" href=\"' + href +\n '\" aria-hidden=\"true\" tabindex=\"-1\">#</a>' +\n '</h' + level + '>'\n )\n })\n}\n\n// A post page's Contents list: the post's sections, i.e. its headings of the\n// topmost level it uses, each with the address that jumps to it; none for a\n// post of fewer than BLOG_CONTENTS_MIN_SECTIONS sections.\n// MUST mirror contentsSectionsOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.\nfunction blogContentsSectionsOf(headings) {\n if (!Array.isArray(headings) || headings.length === 0) return []\n var top = 6\n for (var i = 0; i < headings.length; i++) {\n if (headings[i].level < top) top = headings[i].level\n }\n var sections = []\n for (var j = 0; j < headings.length; j++) {\n if (headings[j].level !== top) continue\n sections.push({ id: headings[j].id, title: headings[j].text, href: '#' + headings[j].id })\n }\n return sections.length >= BLOG_CONTENTS_MIN_SECTIONS ? sections : []\n}\n"); };
28
52
  exports.generateHeadingAnchorsCode = generateHeadingAnchorsCode;
29
53
  //# sourceMappingURL=heading-anchors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"heading-anchors.js","sourceRoot":"","sources":["../../../src/transformations/heading-anchors.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACU,QAAA,yBAAyB,GAAG,mBAAmB,CAAA;AAE5D;;;;;;;;;;;;;GAaG;AACI,IAAM,0BAA0B,GAAG,cAAc,OAAA,6CACrB,iCAAyB,64DA2D3D,EA5DuD,CA4DvD,CAAA;AA5DY,QAAA,0BAA0B,8BA4DtC"}
1
+ {"version":3,"file":"heading-anchors.js","sourceRoot":"","sources":["../../../src/transformations/heading-anchors.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACU,QAAA,yBAAyB,GAAG,mBAAmB,CAAA;AAE5D;;;;;;GAMG;AACU,QAAA,uBAAuB,GAAG,iBAAiB,CAAA;AAExD;;;;;;GAMG;AACU,QAAA,qBAAqB,GAAG,CAAC,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,IAAM,0BAA0B,GAAG,cAAc,OAAA,6CACrB,iCAAyB,+CAC3B,+BAAuB,mJAEG,iCAAyB,2EACjD,6BAAqB,s9GA4FvD,EAjGuD,CAiGvD,CAAA;AAjGY,QAAA,0BAA0B,8BAiGtC"}
@@ -1,4 +1,5 @@
1
1
  import type { GeneratorOptions, UIDLAuthentication, UIDLEcommerceCategory } from '@teleporthq/teleport-types';
2
+ import { ContentTables } from '@teleporthq/teleport-shared';
2
3
  import { type EcommerceProductTransformOptions } from './ecommerce-product';
3
4
  import { type ContentLocalization, type LocalizedProjectOptions } from '../content-localization';
4
5
  export type { EcommerceProductTransformOptions };
@@ -8,7 +9,21 @@ export type { EcommerceProductTransformOptions };
8
9
  * to serve whichever transform that resolves to — the product fields are ignored
9
10
  * for a blog table and vice versa.
10
11
  */
12
+ /** What a content preset bakes into its post transform — see `UIDLBlogSettings`. */
13
+ export interface ContentPresetTransformSettings {
14
+ categories?: UIDLEcommerceCategory[];
15
+ headingAnchors?: boolean;
16
+ contents?: boolean;
17
+ comments?: boolean;
18
+ order?: 'manual';
19
+ }
11
20
  export interface EntityTransformOptions extends EcommerceProductTransformOptions {
21
+ /**
22
+ * The settings of every content preset, by preset key — the post transform
23
+ * of a table reads its own preset's (`contentSettingsFor`). The `blog*`
24
+ * fields below are the Blog's, kept for the callers that name them.
25
+ */
26
+ contentSettings?: Partial<Record<ContentTables.ContentPresetKey, ContentPresetTransformSettings>>;
12
27
  /** Blog post-category taxonomy — see `blogSettings.categories`. */
13
28
  blogCategories?: UIDLEcommerceCategory[];
14
29
  /** Post content headings get ids and `#` links — see `blogSettings.headingAnchors`. */
@@ -34,15 +49,12 @@ export interface EntityTransformOptions extends EcommerceProductTransformOptions
34
49
  trustedReaderRoles?: string[];
35
50
  }
36
51
  /**
37
- * Collects everything the product transform bakes in from the generator
38
- * options, in ONE place so every fetcher-emitting call site stays in step.
39
- *
40
- * `storefrontTaxRate` is what turns a NET catalogue price into the price the
41
- * shopper is quoted; `resolveStorefrontTaxRate` returns 0 (a no-op) for
42
- * tax-inclusive or untaxed stores. `allowBackorders` is the effective
43
- * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
52
+ * The settings the transform of `tableName` bakes in: its preset's entry in
53
+ * `contentSettings`, or — for the Blog, and for a caller that only set the
54
+ * `blog*` fields — those fields. Empty for a table no preset owns.
44
55
  */
45
- export declare const buildProductTransformOptions: (options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'auth'> & LocalizedProjectOptions & {
56
+ export declare const contentSettingsFor: (options: Pick<EntityTransformOptions, 'contentSettings' | 'blogCategories' | 'blogHeadingAnchors' | 'blogComments'>, tableName: string) => ContentPresetTransformSettings;
57
+ export declare const buildProductTransformOptions: (options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'helpCenterSettings' | 'auth'> & LocalizedProjectOptions & {
46
58
  /**
47
59
  * The SAME project authentication under the name a `ProjectUIDL` gives
48
60
  * it — the project plugins call this with the UIDL itself, where the
@@ -59,9 +71,9 @@ export type TransformationType = 'blog-post' | 'ecommerce-product' | 'custom-pag
59
71
  * Detects which transformation type to apply based on the table name.
60
72
  * Returns null if no transformation is needed.
61
73
  *
62
- * IMPORTANT: Only the real platform-managed tables (`teleport_products`,
63
- * `teleport_blog_posts`) are routed through the e-commerce/blog view-model
64
- * transforms. A previous loose substring match (`lower.includes('products')`)
74
+ * IMPORTANT: Only the real platform-managed tables (`teleport_products`, the
75
+ * content presets' posts tables) are routed through the e-commerce/blog
76
+ * view-model transforms. A previous loose substring match (`lower.includes('products')`)
65
77
  * incorrectly routed CUSTOM tables such as `products`, `store_products` or
66
78
  * `wholesale_products` through `buildEcommerceProduct`, which emits a fixed set
67
79
  * of platform product fields and silently drops all custom columns. Matching the
@@ -119,5 +131,5 @@ export declare const REVIEWS_PER_PRODUCT = 5;
119
131
  * asked of every caller. Without a localization every row reads as before —
120
132
  * the base columns.
121
133
  */
122
- export declare const getTransformWrapperCode: (tableName: string, options?: Pick<EntityTransformOptions, 'localization' | 'blogComments'>) => string;
134
+ export declare const getTransformWrapperCode: (tableName: string, options?: Pick<EntityTransformOptions, 'localization' | 'blogComments' | 'contentSettings'>) => string;
123
135
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transformations/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,4BAA4B,CAAA;AAKnC,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC7B,MAAM,yBAAyB,CAAA;AAEhC,YAAY,EAAE,gCAAgC,EAAE,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,WAAW,sBAAuB,SAAQ,gCAAgC;IAC9E,mEAAmE;IACnE,cAAc,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACxC,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAkBD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,YAC9B,KACP,gBAAgB,EAChB,mBAAmB,GAAG,iBAAiB,GAAG,cAAc,GAAG,MAAM,CAClE,GACC,uBAAuB,GAAG;IACxB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,kBAAkB,CAAA;CACpC,KACF,sBAUD,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,mBAAmB,GAAG,aAAa,GAAG,IAAI,CAAA;AAezF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,cAAe,MAAM,KAAG,kBAe5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,cACrB,MAAM,YACR,sBAAsB,KAC9B,MAwBF,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,YAAY,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,cAAe,MAAM,KAAG,MAAM,GAAG,IAKnE,CAAA;AA+DD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,cACvB,MAAM,YACR,KAAK,sBAAsB,EAAE,cAAc,GAAG,cAAc,CAAC,KACrE,MAkKF,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transformations/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAEhB,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,aAAa,EAA8B,MAAM,6BAA6B,CAAA;AAIvF,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC7B,MAAM,yBAAyB,CAAA;AAEhC,YAAY,EAAE,gCAAgC,EAAE,CAAA;AAEhD;;;;;GAKG;AACH,oFAAoF;AACpF,MAAM,WAAW,8BAA8B;IAC7C,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACpC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,QAAQ,CAAA;CACjB;AAED,MAAM,WAAW,sBAAuB,SAAQ,gCAAgC;IAC9E;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,EAAE,8BAA8B,CAAC,CAAC,CAAA;IACjG,mEAAmE;IACnE,cAAc,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACxC,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAqCD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,YACpB,KACP,sBAAsB,EACtB,iBAAiB,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,cAAc,CAC7E,aACU,MAAM,KAChB,8BAiBF,CAAA;AAED,eAAO,MAAM,4BAA4B,YAC9B,KACP,gBAAgB,EAChB,mBAAmB,GAAG,iBAAiB,GAAG,cAAc,GAAG,oBAAoB,GAAG,MAAM,CACzF,GACC,uBAAuB,GAAG;IACxB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,kBAAkB,CAAA;CACpC,KACF,sBAcD,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,mBAAmB,GAAG,aAAa,GAAG,IAAI,CAAA;AAezF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,cAAe,MAAM,KAAG,kBAe5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,cACrB,MAAM,YACR,sBAAsB,KAC9B,MA8BF,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,YAAY,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,cAAe,MAAM,KAAG,MAAM,GAAG,IAKnE,CAAA;AA+DD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,cACvB,MAAM,YACR,KAAK,sBAAsB,EAAE,cAAc,GAAG,cAAc,GAAG,iBAAiB,CAAC,KACzF,MAmKF,CAAA"}