@thallylabs/migrate 0.2.41 → 0.2.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +106 -23
- package/dist/index.js.map +1 -1
- package/dist/url-migration.d.ts +2 -2
- package/dist/url-migration.js +106 -23
- package/dist/url-migration.js.map +1 -1
- package/dist/{url-run-mSKFqHx_.d.ts → url-run-CPzQfjRW.d.ts} +2 -0
- package/package.json +2 -1
package/dist/url-migration.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h as MigrationCapture } from './url-run-
|
|
2
|
-
export { a as MigrationAsset, f as MigrationBundle, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, g as MigrationDocsConfig, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, d as MigrationFetcher, o as MigrationJournalAsset, b as MigrationNavigationGroup, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, M as MigrationPage, e as MigrationPlatform, c as MigrationWarning, R as RenderedMigrationFile, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-run-
|
|
1
|
+
import { h as MigrationCapture } from './url-run-CPzQfjRW.js';
|
|
2
|
+
export { a as MigrationAsset, f as MigrationBundle, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, g as MigrationDocsConfig, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, d as MigrationFetcher, o as MigrationJournalAsset, b as MigrationNavigationGroup, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, M as MigrationPage, e as MigrationPlatform, c as MigrationWarning, R as RenderedMigrationFile, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-run-CPzQfjRW.js';
|
|
3
3
|
|
|
4
4
|
interface UrlMigrationScope {
|
|
5
5
|
scopePath: string;
|
package/dist/url-migration.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/url-run.ts
|
|
2
2
|
import { createHash as createHash4 } from "crypto";
|
|
3
3
|
import { load as load7 } from "cheerio";
|
|
4
|
+
import { parse as parse2, postprocess, preprocess } from "micromark";
|
|
4
5
|
import remarkParse4 from "remark-parse";
|
|
5
6
|
import remarkMdx4 from "remark-mdx";
|
|
6
7
|
import { unified as unified4 } from "unified";
|
|
@@ -1815,7 +1816,10 @@ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
1815
1816
|
"width",
|
|
1816
1817
|
"height",
|
|
1817
1818
|
"preload",
|
|
1818
|
-
"aria-label"
|
|
1819
|
+
"aria-label",
|
|
1820
|
+
// Publisher asset provenance does not change playback and is never emitted.
|
|
1821
|
+
// Keep this explicit: arbitrary data attributes may drive custom controls.
|
|
1822
|
+
"data-path"
|
|
1819
1823
|
]);
|
|
1820
1824
|
var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1821
1825
|
"src",
|
|
@@ -5216,9 +5220,9 @@ function apiOperationCodeControls(body, specification, operation) {
|
|
|
5216
5220
|
}
|
|
5217
5221
|
|
|
5218
5222
|
// src/url-run.ts
|
|
5219
|
-
var CONVERTER_VERSION = "url-evidence-
|
|
5223
|
+
var CONVERTER_VERSION = "url-evidence-v7";
|
|
5220
5224
|
var parser2 = unified4().use(remarkParse4).use(remarkMdx4);
|
|
5221
|
-
var ACCEPT = "text/
|
|
5225
|
+
var ACCEPT = "text/html,application/xhtml+xml";
|
|
5222
5226
|
var MAX_RESPONSE_BYTES = 8e6;
|
|
5223
5227
|
var MAX_ASSET_BYTES = 1e7;
|
|
5224
5228
|
var MAX_RECORDS = 1e5;
|
|
@@ -5248,6 +5252,11 @@ function canonical(value, base) {
|
|
|
5248
5252
|
return null;
|
|
5249
5253
|
}
|
|
5250
5254
|
}
|
|
5255
|
+
function sameRouteMarkdownRepresentation(alternate, primary) {
|
|
5256
|
+
const target2 = new URL(alternate), source = new URL(primary);
|
|
5257
|
+
const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
|
|
5258
|
+
return target2.origin === source.origin && target2.search === source.search && directory2(target2.pathname) === directory2(source.pathname) && /\.mdx?$/i.test(target2.pathname);
|
|
5259
|
+
}
|
|
5251
5260
|
function classifyMigrationResponse(document) {
|
|
5252
5261
|
if (document.status && document.status >= 400) return "unsupported";
|
|
5253
5262
|
if (/\/(?:llms(?:-full)?|robots)\.txt$|\/sitemap[^/]*\.xml$|\/search(?:\.html)?$/i.test(document.finalUrl.pathname) || /\.(?:json|ya?ml)$/i.test(document.finalUrl.pathname) || /(?:xml|json)/i.test(document.contentType)) return "index";
|
|
@@ -5498,7 +5507,7 @@ function resolveMigrationRecord(records, record) {
|
|
|
5498
5507
|
}
|
|
5499
5508
|
return record;
|
|
5500
5509
|
}
|
|
5501
|
-
function navigationConfig(checkpoint, records) {
|
|
5510
|
+
function navigationConfig(checkpoint, records, occurrencePages = /* @__PURE__ */ new Map()) {
|
|
5502
5511
|
const navigation = checkpoint.navigation;
|
|
5503
5512
|
const shortcuts = [];
|
|
5504
5513
|
const collectShortcuts = (nodes) => {
|
|
@@ -5523,7 +5532,7 @@ function navigationConfig(checkpoint, records) {
|
|
|
5523
5532
|
const project = (nodes) => nodes.flatMap((node) => {
|
|
5524
5533
|
const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
|
|
5525
5534
|
const target2 = resolveMigrationRecord(records, record);
|
|
5526
|
-
const page = target2?.page;
|
|
5535
|
+
const page = occurrencePages.get(node) ?? target2?.page;
|
|
5527
5536
|
const children = project(node.children ?? []);
|
|
5528
5537
|
if (children.length) {
|
|
5529
5538
|
const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === (page && projection.navigationId(page)) : contains(entry.pages));
|
|
@@ -5823,8 +5832,19 @@ async function runUrlMigration(options) {
|
|
|
5823
5832
|
if (!record.evidence) continue;
|
|
5824
5833
|
const final = canonical(record.evidence.finalUrl, sourceUrl);
|
|
5825
5834
|
if (!final) continue;
|
|
5826
|
-
if (record.outcome === "imported")
|
|
5827
|
-
|
|
5835
|
+
if (record.outcome === "imported") {
|
|
5836
|
+
enqueueArticleLinks(record.evidence.links, final, record.url);
|
|
5837
|
+
for (const reference of record.evidence.markdownRepresentations ?? []) {
|
|
5838
|
+
const alternate = canonical(reference, final);
|
|
5839
|
+
if (!alternate || !sameRouteMarkdownRepresentation(alternate, final)) continue;
|
|
5840
|
+
enqueue(alternate, final, "declared-markdown-representation");
|
|
5841
|
+
const sibling = records.get(alternate);
|
|
5842
|
+
if (sibling && sibling.representationOf !== record.url) {
|
|
5843
|
+
sibling.representationOf = record.url;
|
|
5844
|
+
changed.push(sibling);
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
} else if (record.outcome === "excluded") {
|
|
5828
5848
|
if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
|
|
5829
5849
|
for (const link of record.evidence.links) enqueue(link, final, record.url);
|
|
5830
5850
|
}
|
|
@@ -5907,6 +5927,7 @@ async function runUrlMigration(options) {
|
|
|
5907
5927
|
let claimedId;
|
|
5908
5928
|
let claimedFinal;
|
|
5909
5929
|
let markdownRepresentation;
|
|
5930
|
+
const markdownRepresentations = [];
|
|
5910
5931
|
let exactKey;
|
|
5911
5932
|
try {
|
|
5912
5933
|
delete record.reason;
|
|
@@ -5999,13 +6020,13 @@ async function runUrlMigration(options) {
|
|
|
5999
6020
|
$('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
|
|
6000
6021
|
const alternate = canonical($(node).attr("href"), final);
|
|
6001
6022
|
if (!alternate) return;
|
|
6002
|
-
|
|
6003
|
-
const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
|
|
6004
|
-
if (target2.origin !== primary.origin || target2.search !== primary.search || directory2(target2.pathname) !== directory2(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
|
|
6023
|
+
if (!sameRouteMarkdownRepresentation(alternate, final)) return;
|
|
6005
6024
|
enqueue(alternate, final, "declared-markdown-representation");
|
|
6006
6025
|
const sibling = records.get(alternate);
|
|
6026
|
+
if (!sibling) return;
|
|
6007
6027
|
sibling.representationOf = record.url;
|
|
6008
6028
|
markdownRepresentation ??= alternate;
|
|
6029
|
+
markdownRepresentations.push(alternate);
|
|
6009
6030
|
changed.push(sibling);
|
|
6010
6031
|
});
|
|
6011
6032
|
}
|
|
@@ -6018,7 +6039,8 @@ async function runUrlMigration(options) {
|
|
|
6018
6039
|
}
|
|
6019
6040
|
enqueueNavigation(observedNavigation, final);
|
|
6020
6041
|
}
|
|
6021
|
-
|
|
6042
|
+
const replacesRepresentation = imported >= maxPages && markdownRepresentations.some((url) => records.get(url)?.page);
|
|
6043
|
+
if (imported >= maxPages && !replacesRepresentation) {
|
|
6022
6044
|
record.outcome = "excluded";
|
|
6023
6045
|
record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
|
|
6024
6046
|
return;
|
|
@@ -6037,14 +6059,17 @@ async function runUrlMigration(options) {
|
|
|
6037
6059
|
const locale = language.locale;
|
|
6038
6060
|
const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
|
|
6039
6061
|
const identityId = locale ? `${locale}/${navigationId}` : id;
|
|
6040
|
-
|
|
6062
|
+
const owner = records.get(usedIds.get(identityId) ?? "");
|
|
6063
|
+
const sharesDeclaredRepresentation = owner && (owner.representationOf === record.url || record.representationOf === owner.url);
|
|
6064
|
+
if (owner && owner !== record && !sharesDeclaredRepresentation) {
|
|
6041
6065
|
record.outcome = "failed";
|
|
6042
6066
|
record.reason = `Destination collision for ${identityId}.`;
|
|
6043
6067
|
return;
|
|
6044
6068
|
}
|
|
6045
|
-
usedIds.set(identityId, record.url);
|
|
6069
|
+
if (!owner || owner === record || owner.representationOf === record.url) usedIds.set(identityId, record.url);
|
|
6046
6070
|
claimedId = identityId;
|
|
6047
6071
|
const evidence = evidenceOf(document);
|
|
6072
|
+
if (markdownRepresentations.length) evidence.markdownRepresentations = markdownRepresentations;
|
|
6048
6073
|
const page = convert(document, id);
|
|
6049
6074
|
if (!page) {
|
|
6050
6075
|
record.outcome = "failed";
|
|
@@ -6255,6 +6280,7 @@ ${difference}`;
|
|
|
6255
6280
|
if (!primary || primary === record) continue;
|
|
6256
6281
|
const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
|
|
6257
6282
|
if (!winner) continue;
|
|
6283
|
+
if (winner.page) usedIds.set(winner.page.id, winner.url);
|
|
6258
6284
|
const alternate = winner === primary ? record : primary;
|
|
6259
6285
|
if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
|
|
6260
6286
|
if (alternate.outcome === "imported") {
|
|
@@ -6276,7 +6302,17 @@ ${difference}`;
|
|
|
6276
6302
|
settleArticleIdentities();
|
|
6277
6303
|
while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
|
|
6278
6304
|
if (imported >= maxPages) {
|
|
6279
|
-
const index = pending.findIndex((record, position) =>
|
|
6305
|
+
const index = pending.findIndex((record, position) => {
|
|
6306
|
+
if (position < cursor || record.outcome !== "pending") return false;
|
|
6307
|
+
const url = new URL(record.url);
|
|
6308
|
+
if (isDiscoveryIndex(url, source)) return true;
|
|
6309
|
+
if (/\.mdx?$/i.test(url.pathname)) return false;
|
|
6310
|
+
return [".md", ".mdx", "/index.md", "/index.mdx"].some((extension) => {
|
|
6311
|
+
const alternate = new URL(url);
|
|
6312
|
+
alternate.pathname = alternate.pathname.replace(/\/index$/i, "") + extension;
|
|
6313
|
+
return records.get(alternate.href)?.outcome === "imported";
|
|
6314
|
+
});
|
|
6315
|
+
});
|
|
6280
6316
|
if (index < 0) break;
|
|
6281
6317
|
pending.splice(cursor, 0, pending.splice(index, 1)[0]);
|
|
6282
6318
|
}
|
|
@@ -6300,19 +6336,60 @@ ${difference}`;
|
|
|
6300
6336
|
}
|
|
6301
6337
|
const navigationLabels = /* @__PURE__ */ new Map();
|
|
6302
6338
|
const navigationIssues = [];
|
|
6339
|
+
const navigationOccurrences = /* @__PURE__ */ new Map();
|
|
6303
6340
|
const labelPages = (nodes) => {
|
|
6304
6341
|
for (const node of nodes) {
|
|
6305
6342
|
const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
|
|
6306
6343
|
const target2 = resolveMigrationRecord(records, entry);
|
|
6307
|
-
if (target2?.page && node.label && !node.children?.length && !entry
|
|
6308
|
-
const
|
|
6309
|
-
|
|
6310
|
-
|
|
6344
|
+
if (entry && target2?.page && node.label && !node.children?.length && !entry.aliasAnchor) {
|
|
6345
|
+
const occurrences = navigationOccurrences.get(target2) ?? [];
|
|
6346
|
+
occurrences.push({ node, entry });
|
|
6347
|
+
navigationOccurrences.set(target2, occurrences);
|
|
6311
6348
|
}
|
|
6312
6349
|
labelPages(node.children ?? []);
|
|
6313
6350
|
}
|
|
6314
6351
|
};
|
|
6315
6352
|
labelPages(checkpoint.navigation);
|
|
6353
|
+
const occurrencePages = /* @__PURE__ */ new Map();
|
|
6354
|
+
const navigationAliases = /* @__PURE__ */ new Map();
|
|
6355
|
+
const occupiedPageIds = new Set(checkpoint.records.flatMap((record) => record.page ? [projection.pageId(record.page)] : []));
|
|
6356
|
+
const navigationRedirects = [];
|
|
6357
|
+
for (const [target2, occurrences] of navigationOccurrences) {
|
|
6358
|
+
const final = target2.evidence && canonical(target2.evidence.finalUrl, sourceUrl);
|
|
6359
|
+
const primary = occurrences.find(({ entry }) => entry.url === final) ?? occurrences.find(({ entry }) => entry === target2) ?? occurrences[0];
|
|
6360
|
+
navigationLabels.set(target2.url, primary.node.label);
|
|
6361
|
+
for (const { node, entry } of occurrences) {
|
|
6362
|
+
if (node.label === primary.node.label) continue;
|
|
6363
|
+
const existing = navigationAliases.get(entry.url);
|
|
6364
|
+
if (existing?.page.navTitle === node.label && existing.target === target2) {
|
|
6365
|
+
occurrencePages.set(node, existing.page);
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
6368
|
+
const sourceRoute = new URL(entry.url);
|
|
6369
|
+
const id = inScope(sourceRoute, source, scopePath) ? idForUrl(sourceRoute, scopePath) : null;
|
|
6370
|
+
const locale = projection.canonicalLocale(target2.page);
|
|
6371
|
+
let navigationId = id && (locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id);
|
|
6372
|
+
const legacyPrefix = locale && `${locale.toLowerCase()}-`;
|
|
6373
|
+
if (navigationId && legacyPrefix && projection.navigationId(target2.page) !== target2.page.navigationId && navigationId.toLowerCase().startsWith(legacyPrefix)) navigationId = navigationId.slice(legacyPrefix.length);
|
|
6374
|
+
const page = navigationId ? {
|
|
6375
|
+
...target2.page,
|
|
6376
|
+
id: locale ? `${locale}/${navigationId}` : navigationId,
|
|
6377
|
+
navigationId,
|
|
6378
|
+
...locale ? { locale } : {},
|
|
6379
|
+
navTitle: node.label,
|
|
6380
|
+
source: entry.url,
|
|
6381
|
+
noindex: true
|
|
6382
|
+
} : void 0;
|
|
6383
|
+
if (!page || existing || occupiedPageIds.has(projection.pageId(page))) {
|
|
6384
|
+
navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${primary.node.label}, ${node.label}); their source routes cannot retain distinct sidebar titles safely.` });
|
|
6385
|
+
continue;
|
|
6386
|
+
}
|
|
6387
|
+
occupiedPageIds.add(projection.pageId(page));
|
|
6388
|
+
navigationAliases.set(entry.url, { page, target: target2 });
|
|
6389
|
+
occurrencePages.set(node, page);
|
|
6390
|
+
navigationRedirects.push({ source: projection.route(page), destination: projection.route(target2.page), permanent: true });
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6316
6393
|
const pages = checkpoint.records.flatMap((record) => record.page ? [{ ...record.page, id: projection.pageId(record.page), navigationId: projection.navigationId(record.page), ...projection.canonicalLocale(record.page) ? { locale: projection.canonicalLocale(record.page) } : {}, ...navigationLabels.has(record.url) ? { navTitle: navigationLabels.get(record.url) } : {}, body: rewriteMarkdownDestinations(record.page.body, (value) => {
|
|
6317
6394
|
let url;
|
|
6318
6395
|
try {
|
|
@@ -6333,6 +6410,10 @@ ${difference}`;
|
|
|
6333
6410
|
const hasBrokenSourceAnchor = Boolean(url.hash && destination?.outcome === "imported" && destination.evidence && !destination.evidence.anchors.includes(anchor));
|
|
6334
6411
|
return `${route}${hasBrokenSourceAnchor ? "" : effectiveHash}`;
|
|
6335
6412
|
}) }] : []);
|
|
6413
|
+
const projectedPages = new Map(pages.map((page) => [page.id, page]));
|
|
6414
|
+
for (const { page, target: target2 } of navigationAliases.values()) {
|
|
6415
|
+
pages.push({ ...page, id: projection.pageId(page), body: projectedPages.get(projection.pageId(target2.page)).body });
|
|
6416
|
+
}
|
|
6336
6417
|
const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
|
|
6337
6418
|
validation.issues.push(...navigationIssues);
|
|
6338
6419
|
if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
|
|
@@ -6353,12 +6434,12 @@ ${difference}`;
|
|
|
6353
6434
|
platform: options.platform ?? "generic",
|
|
6354
6435
|
pages,
|
|
6355
6436
|
assets: [...assets.values()],
|
|
6356
|
-
docsConfig: navigationConfig(checkpoint, records) ?? buildNavigationFromPages(pages),
|
|
6437
|
+
docsConfig: navigationConfig(checkpoint, records, occurrencePages) ?? buildNavigationFromPages(pages),
|
|
6357
6438
|
warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
|
|
6358
6439
|
urlMigrationValidation: validation,
|
|
6359
6440
|
stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
|
|
6360
6441
|
};
|
|
6361
|
-
if (projection.redirects.length) bundle.docsConfig.redirects = projection.redirects;
|
|
6442
|
+
if (projection.redirects.length || navigationRedirects.length) bundle.docsConfig.redirects = [...projection.redirects, ...navigationRedirects];
|
|
6362
6443
|
if (projection.locales.length) {
|
|
6363
6444
|
const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
|
|
6364
6445
|
bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
|
|
@@ -6376,9 +6457,11 @@ function rewriteMarkdownDestinations(body, rewrite) {
|
|
|
6376
6457
|
const original = body.slice(start, end);
|
|
6377
6458
|
const replacement = rewrite(node.url);
|
|
6378
6459
|
if (replacement === node.url) return null;
|
|
6379
|
-
const
|
|
6380
|
-
|
|
6381
|
-
|
|
6460
|
+
const destinations = postprocess(parse2().document().write(preprocess()(original, void 0, true))).filter(([event, token]) => event === "enter" && ["resourceDestinationString", "definitionDestinationString", "autolinkProtocol", "autolinkEmail"].includes(token.type));
|
|
6461
|
+
const destination = destinations.at(-1)?.[1];
|
|
6462
|
+
if (!destination) return null;
|
|
6463
|
+
const escaped = replacement.replace(/[\\&()<>]/g, "\\$&").replace(/[\t\n\r ]/g, (character) => encodeURIComponent(character));
|
|
6464
|
+
return { start: start + destination.start.offset, end: start + destination.end.offset, replacement: escaped };
|
|
6382
6465
|
}).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
|
|
6383
6466
|
for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
|
|
6384
6467
|
if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
|