@contentrain/wp-import 0.7.0 → 0.9.0
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 +106 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +908 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -525,6 +525,7 @@ async function parseWxr(input, opts) {
|
|
|
525
525
|
mime: guessMime(r.attachment_url ?? r.guid ?? ""),
|
|
526
526
|
parent: r.parent,
|
|
527
527
|
parent_resolved: r.parent ? byId.has(r.parent) : null,
|
|
528
|
+
link: !r.parent || byId.get(r.parent)?.status === "publish" && !byId.get(r.parent)?.password ? r.link || null : null,
|
|
528
529
|
author: r.author,
|
|
529
530
|
date: r.date,
|
|
530
531
|
status: r.status,
|
|
@@ -675,6 +676,479 @@ function guessMime(u) {
|
|
|
675
676
|
return MIME[(u.split("?")[0]?.split(".").pop() ?? "").toLowerCase()] ?? null;
|
|
676
677
|
}
|
|
677
678
|
//#endregion
|
|
679
|
+
//#region src/acf.ts
|
|
680
|
+
/** ACF types that never carry content (layout) or must never be read (secrets). */
|
|
681
|
+
const ACF_LAYOUT_TYPES = new Set([
|
|
682
|
+
"tab",
|
|
683
|
+
"accordion",
|
|
684
|
+
"message"
|
|
685
|
+
]);
|
|
686
|
+
const ACF_SECRET_TYPES = new Set(["password"]);
|
|
687
|
+
/**
|
|
688
|
+
* ACF type → Contentrain type, for types whose value maps without a lookup.
|
|
689
|
+
* `select` with `multiple` and `checkbox` are arrays of their options; see `acfFieldDef`.
|
|
690
|
+
*/
|
|
691
|
+
const ACF_SCALAR_TYPES = {
|
|
692
|
+
text: "string",
|
|
693
|
+
textarea: "text",
|
|
694
|
+
wysiwyg: "richtext",
|
|
695
|
+
email: "email",
|
|
696
|
+
url: "url",
|
|
697
|
+
oembed: "url",
|
|
698
|
+
number: "number",
|
|
699
|
+
range: "number",
|
|
700
|
+
true_false: "boolean",
|
|
701
|
+
date_picker: "date",
|
|
702
|
+
date_time_picker: "datetime",
|
|
703
|
+
time_picker: "string",
|
|
704
|
+
color_picker: "color",
|
|
705
|
+
icon_picker: "icon",
|
|
706
|
+
select: "select",
|
|
707
|
+
radio: "select",
|
|
708
|
+
button_group: "select",
|
|
709
|
+
image: "image",
|
|
710
|
+
file: "file"
|
|
711
|
+
};
|
|
712
|
+
const ACF_REFERENCE_TYPES = {
|
|
713
|
+
post_object: "post",
|
|
714
|
+
relationship: "post",
|
|
715
|
+
taxonomy: "term",
|
|
716
|
+
user: "user",
|
|
717
|
+
gallery: "media",
|
|
718
|
+
page_link: "address"
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Names that mean a secret whatever the field's type, for sources that do not state the type: whole words
|
|
722
|
+
* of the name (`user_pass`, `apiKey`, `client-secret`), so `passage_text` or `compass` stay content.
|
|
723
|
+
*/
|
|
724
|
+
const SECRET_NAME = /(?:^|_)(?:pass(?:word|wd)?|secret|token|api_?key|private_?key|credentials?)(?:_|$)/i;
|
|
725
|
+
const secretName = (name) => SECRET_NAME.test(name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_"));
|
|
726
|
+
/** Whether a field must never be read: a secret by type, or — when the type is unknown — by name. */
|
|
727
|
+
function acfIsSecret(name, type) {
|
|
728
|
+
if (type) return ACF_SECRET_TYPES.has(type);
|
|
729
|
+
return secretName(name);
|
|
730
|
+
}
|
|
731
|
+
const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
732
|
+
const SOURCE = "_source";
|
|
733
|
+
/** The ACF type SCF states for a sub-field of a row or group (`<name>_source.type`). */
|
|
734
|
+
const statedType = (row, k) => {
|
|
735
|
+
const s = row[`${k}${SOURCE}`];
|
|
736
|
+
return isRecord(s) && typeof s.type === "string" ? s.type : void 0;
|
|
737
|
+
};
|
|
738
|
+
/** A sub-field that carries nothing to keep: a secret (stated, or by name) or a layout field. */
|
|
739
|
+
const droppedKey = (row, k) => {
|
|
740
|
+
const t = statedType(row, k);
|
|
741
|
+
return t !== void 0 && ACF_LAYOUT_TYPES.has(t) || acfIsSecret(k, t);
|
|
742
|
+
};
|
|
743
|
+
/**
|
|
744
|
+
* An ACF value with every secret sub-field removed, at any depth: repeater rows, groups, flexible layouts.
|
|
745
|
+
* A sub-field's `_source` keeps only its type and label — its `formatted_value` repeats the value.
|
|
746
|
+
*/
|
|
747
|
+
function acfScrub(value, secret = /* @__PURE__ */ new Set()) {
|
|
748
|
+
if (Array.isArray(value)) {
|
|
749
|
+
const rows = value.filter(isRecord);
|
|
750
|
+
const shared = new Set([...secret, ...rows.flatMap((r) => Object.keys(r).filter((k) => !k.endsWith(SOURCE) && droppedKey(r, k)))]);
|
|
751
|
+
return value.map((v) => acfScrub(v, shared));
|
|
752
|
+
}
|
|
753
|
+
if (!isRecord(value)) return value;
|
|
754
|
+
const gone = (k) => secret.has(k) || droppedKey(value, k);
|
|
755
|
+
const out = {};
|
|
756
|
+
for (const [k, v] of Object.entries(value)) {
|
|
757
|
+
if (k.endsWith(SOURCE) && k.length > 7) {
|
|
758
|
+
if (gone(k.slice(0, -7)) || !isRecord(v)) continue;
|
|
759
|
+
out[k] = {
|
|
760
|
+
...typeof v.type === "string" ? { type: v.type } : {},
|
|
761
|
+
...typeof v.label === "string" ? { label: v.label } : {}
|
|
762
|
+
};
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (gone(k)) continue;
|
|
766
|
+
out[k] = acfScrub(v);
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
/** ACF's image/file array (`return_format: array`): `{ ID, url, filename, … }`. */
|
|
771
|
+
const isAttachment = (v) => isRecord(v) && typeof v.url === "string" && ("ID" in v || "id" in v) && ("filename" in v || "mime_type" in v);
|
|
772
|
+
const isLink = (v) => isRecord(v) && typeof v.url === "string" && "title" in v && "target" in v && Object.keys(v).length <= 3;
|
|
773
|
+
const isMap = (v) => isRecord(v) && typeof v.lat === "number" && typeof v.lng === "number";
|
|
774
|
+
/** A date picker's stored value (`Ymd`) → ISO date. */
|
|
775
|
+
const ymd = (v) => typeof v === "string" && /^\d{8}$/.test(v) ? `${v.slice(0, 4)}-${v.slice(4, 6)}-${v.slice(6, 8)}` : null;
|
|
776
|
+
/** `Y-m-d H:i:s` → ISO local datetime (site time: WordPress stores it without a zone). */
|
|
777
|
+
const dt = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v) ? v.replace(" ", "T") : null;
|
|
778
|
+
const pad = (n) => String(Math.floor(n)).padStart(2, "0");
|
|
779
|
+
/** Minutes east of UTC → `+03:00`. */
|
|
780
|
+
const offsetText = (minutes) => `${minutes < 0 ? "-" : "+"}${pad(Math.abs(minutes) / 60)}:${pad(Math.abs(minutes) % 60)}`;
|
|
781
|
+
/** The offset of an IANA zone at a wall-clock time there (DST included), in minutes east of UTC; `null` for an unknown zone. */
|
|
782
|
+
function zoneOffset(local, timeZone) {
|
|
783
|
+
let fmt;
|
|
784
|
+
try {
|
|
785
|
+
fmt = new Intl.DateTimeFormat("en-US", {
|
|
786
|
+
timeZone,
|
|
787
|
+
hourCycle: "h23",
|
|
788
|
+
year: "numeric",
|
|
789
|
+
month: "2-digit",
|
|
790
|
+
day: "2-digit",
|
|
791
|
+
hour: "2-digit",
|
|
792
|
+
minute: "2-digit",
|
|
793
|
+
second: "2-digit"
|
|
794
|
+
});
|
|
795
|
+
} catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
const wall = Date.parse(`${local}Z`);
|
|
799
|
+
const at = (utc) => {
|
|
800
|
+
const p = Object.fromEntries(fmt.formatToParts(new Date(utc)).map((x) => [x.type, x.value]));
|
|
801
|
+
return (Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(p.hour), Number(p.minute), Number(p.second)) - utc) / 6e4;
|
|
802
|
+
};
|
|
803
|
+
return at(wall - at(wall) * 6e4);
|
|
804
|
+
}
|
|
805
|
+
/** A local ISO datetime with the site's offset for that moment; the local value when the site's zone is unknown. */
|
|
806
|
+
function zonedDateTime(local, zone = {}) {
|
|
807
|
+
const minutes = (zone.timeZone ? zoneOffset(local, zone.timeZone) : null) ?? (typeof zone.gmtOffset === "number" && Number.isFinite(zone.gmtOffset) ? Math.round(zone.gmtOffset * 60) : null);
|
|
808
|
+
return minutes === null ? {
|
|
809
|
+
value: local,
|
|
810
|
+
zoned: false
|
|
811
|
+
} : {
|
|
812
|
+
value: `${local}${offsetText(minutes)}`,
|
|
813
|
+
zoned: true
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const LINK_FIELDS = {
|
|
817
|
+
url: { type: "url" },
|
|
818
|
+
title: { type: "string" },
|
|
819
|
+
target: { type: "string" }
|
|
820
|
+
};
|
|
821
|
+
const MAP_FIELDS = {
|
|
822
|
+
address: { type: "string" },
|
|
823
|
+
lat: { type: "decimal" },
|
|
824
|
+
lng: { type: "decimal" },
|
|
825
|
+
zoom: { type: "integer" }
|
|
826
|
+
};
|
|
827
|
+
/** Contentrain type of a sub-value whose ACF type is not stated (a repeater row, a group member). */
|
|
828
|
+
function inferDef(value) {
|
|
829
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
830
|
+
if (isAttachment(value)) return { type: /\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i.test(value.url) ? "image" : "file" };
|
|
831
|
+
if (isLink(value)) return {
|
|
832
|
+
type: "object",
|
|
833
|
+
fields: LINK_FIELDS
|
|
834
|
+
};
|
|
835
|
+
if (isMap(value)) return {
|
|
836
|
+
type: "object",
|
|
837
|
+
fields: MAP_FIELDS
|
|
838
|
+
};
|
|
839
|
+
if (Array.isArray(value)) {
|
|
840
|
+
if (value.every(isRecord)) return {
|
|
841
|
+
type: "array",
|
|
842
|
+
items: {
|
|
843
|
+
type: "object",
|
|
844
|
+
fields: rowFields(value)
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const item = value.map(inferDef).find(Boolean);
|
|
848
|
+
return {
|
|
849
|
+
type: "array",
|
|
850
|
+
items: item?.type === "object" ? item : item?.type ?? "string"
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (isRecord(value)) return {
|
|
854
|
+
type: "object",
|
|
855
|
+
fields: rowFields([value])
|
|
856
|
+
};
|
|
857
|
+
if (ymd(value)) return { type: "date" };
|
|
858
|
+
const t = byValue(value);
|
|
859
|
+
return t ? { type: t } : null;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* The union of the fields every row states; a name typed two ways falls back to `string`. A sub-field whose ACF
|
|
863
|
+
* type the row states (SCF's `<name>_source`) is typed by the table like a top-level field; a reference inside a
|
|
864
|
+
* row is not resolved, so it — and any sub-field without a stated type — is typed from its value.
|
|
865
|
+
*/
|
|
866
|
+
function rowFields(rows) {
|
|
867
|
+
const out = {};
|
|
868
|
+
for (const row of rows) for (const [k, v] of Object.entries(row)) {
|
|
869
|
+
if (k.endsWith(SOURCE) || droppedKey(row, k)) continue;
|
|
870
|
+
const stated = statedType(row, k);
|
|
871
|
+
const source = row[`${k}${SOURCE}`];
|
|
872
|
+
const label = isRecord(source) && typeof source.label === "string" ? source.label : void 0;
|
|
873
|
+
const d = stated && !ACF_REFERENCE_TYPES[stated] ? acfFieldDef(k, v, {
|
|
874
|
+
type: stated,
|
|
875
|
+
...label ? { label } : {}
|
|
876
|
+
}) : inferDef(v);
|
|
877
|
+
if (!d) continue;
|
|
878
|
+
const seen = out[k];
|
|
879
|
+
if (!seen) out[k] = d;
|
|
880
|
+
else if (seen.type !== d.type) out[k] = { type: "string" };
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
/** A value in the shape its field definition promises; `undefined` = nothing to store. */
|
|
885
|
+
function acfValue(def, value, ctx = {}) {
|
|
886
|
+
if (value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length) return void 0;
|
|
887
|
+
switch (def.type) {
|
|
888
|
+
case "image":
|
|
889
|
+
case "file":
|
|
890
|
+
case "video": return isAttachment(value) ? value.url : typeof value === "string" ? value : void 0;
|
|
891
|
+
case "date": return ymd(value) ?? (typeof value === "string" ? value : void 0);
|
|
892
|
+
case "datetime": {
|
|
893
|
+
const local = dt(value);
|
|
894
|
+
if (!local) return typeof value === "string" ? value : void 0;
|
|
895
|
+
const out = zonedDateTime(local, ctx);
|
|
896
|
+
if (!out.zoned) ctx.unzoned?.(out.value);
|
|
897
|
+
return out.value;
|
|
898
|
+
}
|
|
899
|
+
case "boolean": return value === true || value === 1 || value === "1";
|
|
900
|
+
case "number":
|
|
901
|
+
case "integer":
|
|
902
|
+
case "decimal": return typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : void 0;
|
|
903
|
+
case "icon": return isRecord(value) && typeof value.value === "string" ? value.value : typeof value === "string" ? value : void 0;
|
|
904
|
+
case "object": {
|
|
905
|
+
if (!isRecord(value)) return void 0;
|
|
906
|
+
const out = {};
|
|
907
|
+
for (const [k, sub] of Object.entries(def.fields ?? {})) {
|
|
908
|
+
if (droppedKey(value, k)) continue;
|
|
909
|
+
const v = acfValue(sub, value[k], ctx);
|
|
910
|
+
if (v !== void 0) out[k] = v;
|
|
911
|
+
}
|
|
912
|
+
return Object.keys(out).length ? out : void 0;
|
|
913
|
+
}
|
|
914
|
+
case "array": {
|
|
915
|
+
if (!Array.isArray(value)) return void 0;
|
|
916
|
+
const item = typeof def.items === "string" ? { type: def.items } : def.items ?? { type: "string" };
|
|
917
|
+
const out = value.map((v) => acfValue(item, v, ctx)).filter((v) => v !== void 0);
|
|
918
|
+
return out.length ? out : void 0;
|
|
919
|
+
}
|
|
920
|
+
case "select": {
|
|
921
|
+
const v = typeof value === "string" ? value : typeof value === "number" ? String(value) : void 0;
|
|
922
|
+
if (v !== void 0 && def.options?.length && !def.options.includes(v)) {
|
|
923
|
+
ctx.dropped?.(v, def.options);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
return v;
|
|
927
|
+
}
|
|
928
|
+
case "richtext":
|
|
929
|
+
case "text":
|
|
930
|
+
case "string":
|
|
931
|
+
case "url":
|
|
932
|
+
case "email":
|
|
933
|
+
case "color": return typeof value === "string" ? value : typeof value === "number" ? String(value) : void 0;
|
|
934
|
+
default: return value;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Field definition for one ACF field value, or `null` when it carries no content, is a secret, or is a
|
|
939
|
+
* reference (the caller resolves those: see `ACF_REFERENCE_TYPES`).
|
|
940
|
+
*/
|
|
941
|
+
function acfFieldDef(name, value, stated = {}) {
|
|
942
|
+
const t = stated.type;
|
|
943
|
+
if (t && (ACF_LAYOUT_TYPES.has(t) || ACF_REFERENCE_TYPES[t])) return null;
|
|
944
|
+
if (acfIsSecret(name, t)) return null;
|
|
945
|
+
const label = stated.label ? strip(stated.label) : void 0;
|
|
946
|
+
const withLabel = (d) => label ? {
|
|
947
|
+
...d,
|
|
948
|
+
label
|
|
949
|
+
} : d;
|
|
950
|
+
if (!t) {
|
|
951
|
+
const inferred = inferDef(value);
|
|
952
|
+
return inferred ? withLabel({
|
|
953
|
+
...inferred,
|
|
954
|
+
description: "ACF (type inferred from the value)"
|
|
955
|
+
}) : null;
|
|
956
|
+
}
|
|
957
|
+
const choices = stated.choices?.length ? stated.choices : void 0;
|
|
958
|
+
if (t === "checkbox" || t === "select" && (stated.multiple || Array.isArray(value))) return withLabel({
|
|
959
|
+
type: "array",
|
|
960
|
+
items: choices ? {
|
|
961
|
+
type: "select",
|
|
962
|
+
options: choices
|
|
963
|
+
} : "string"
|
|
964
|
+
});
|
|
965
|
+
if (ACF_SCALAR_TYPES[t]) {
|
|
966
|
+
const type = ACF_SCALAR_TYPES[t];
|
|
967
|
+
if (type === "select") return withLabel(choices ? {
|
|
968
|
+
type,
|
|
969
|
+
options: choices
|
|
970
|
+
} : { type: "string" });
|
|
971
|
+
return withLabel({ type });
|
|
972
|
+
}
|
|
973
|
+
if (t === "link") return withLabel({
|
|
974
|
+
type: "object",
|
|
975
|
+
fields: LINK_FIELDS
|
|
976
|
+
});
|
|
977
|
+
if (t === "google_map") return withLabel({
|
|
978
|
+
type: "object",
|
|
979
|
+
fields: MAP_FIELDS
|
|
980
|
+
});
|
|
981
|
+
if (t === "group") return withLabel({
|
|
982
|
+
type: "object",
|
|
983
|
+
fields: rowFields(isRecord(value) ? [value] : [])
|
|
984
|
+
});
|
|
985
|
+
if (t === "repeater") return withLabel({
|
|
986
|
+
type: "array",
|
|
987
|
+
items: {
|
|
988
|
+
type: "object",
|
|
989
|
+
fields: rowFields(Array.isArray(value) ? value.filter(isRecord) : [])
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
if (t === "flexible_content") {
|
|
993
|
+
const rows = Array.isArray(value) ? value.filter(isRecord) : [];
|
|
994
|
+
const fields = rowFields(rows.map(({ acf_fc_layout: _l, ...rest }) => rest));
|
|
995
|
+
return withLabel({
|
|
996
|
+
type: "array",
|
|
997
|
+
items: {
|
|
998
|
+
type: "object",
|
|
999
|
+
fields: {
|
|
1000
|
+
layout: {
|
|
1001
|
+
type: "select",
|
|
1002
|
+
options: [...new Set(rows.map((r) => String(r.acf_fc_layout ?? "")).filter(Boolean))].toSorted(),
|
|
1003
|
+
required: true
|
|
1004
|
+
},
|
|
1005
|
+
...fields
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const inferred = inferDef(value);
|
|
1011
|
+
return inferred ? withLabel({
|
|
1012
|
+
...inferred,
|
|
1013
|
+
description: `ACF ${t} (type inferred from the value)`
|
|
1014
|
+
}) : null;
|
|
1015
|
+
}
|
|
1016
|
+
/** Merge two definitions of the same field seen on different entries (union of object fields and options). */
|
|
1017
|
+
function mergeFieldDef(a, b) {
|
|
1018
|
+
if (a.type !== b.type) return a;
|
|
1019
|
+
const out = { ...a };
|
|
1020
|
+
if (a.options || b.options) out.options = [...new Set([...a.options ?? [], ...b.options ?? []])].toSorted();
|
|
1021
|
+
if (a.fields || b.fields) {
|
|
1022
|
+
const fields = { ...a.fields };
|
|
1023
|
+
for (const [k, d] of Object.entries(b.fields ?? {})) fields[k] = fields[k] ? mergeFieldDef(fields[k], d) : d;
|
|
1024
|
+
out.fields = fields;
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof a.items === "object" && typeof b.items === "object") out.items = mergeFieldDef(a.items, b.items);
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
/** Flexible content rows keep their layout name under `layout`. */
|
|
1030
|
+
function acfRows(value) {
|
|
1031
|
+
return Array.isArray(value) ? value.map((r) => isRecord(r) && "acf_fc_layout" in r ? (({ acf_fc_layout, ...rest }) => ({
|
|
1032
|
+
layout: acf_fc_layout,
|
|
1033
|
+
...rest
|
|
1034
|
+
}))(r) : r) : value;
|
|
1035
|
+
}
|
|
1036
|
+
//#endregion
|
|
1037
|
+
//#region src/rest-redirects.ts
|
|
1038
|
+
/** Redirection's own defaults, under a site that states none. */
|
|
1039
|
+
const DEFAULTS = {
|
|
1040
|
+
flag_query: "exact",
|
|
1041
|
+
flag_case: false,
|
|
1042
|
+
flag_trailing: false
|
|
1043
|
+
};
|
|
1044
|
+
const MODULES = {
|
|
1045
|
+
2: "apache",
|
|
1046
|
+
3: "nginx"
|
|
1047
|
+
};
|
|
1048
|
+
/** Codes that say an address is gone: served as that answer, with no target. */
|
|
1049
|
+
const GONE = new Set([410, 451]);
|
|
1050
|
+
/** Redirection's query modes; `exactorder` (the same parameters in the same order) is read as `exact`. */
|
|
1051
|
+
const QUERY = {
|
|
1052
|
+
exact: "exact",
|
|
1053
|
+
exactorder: "exact",
|
|
1054
|
+
ignore: "ignore",
|
|
1055
|
+
pass: "pass"
|
|
1056
|
+
};
|
|
1057
|
+
/** The flags one layer states, as Redirection accepts them: a known query mode, a true boolean (not `"1"`). */
|
|
1058
|
+
const valid = (f) => ({
|
|
1059
|
+
...typeof f?.flag_query === "string" && f.flag_query in QUERY ? { flag_query: f.flag_query } : {},
|
|
1060
|
+
...typeof f?.flag_case === "boolean" ? { flag_case: f.flag_case } : {},
|
|
1061
|
+
...typeof f?.flag_trailing === "boolean" ? { flag_trailing: f.flag_trailing } : {}
|
|
1062
|
+
});
|
|
1063
|
+
/** By id as bytes, as the Bridge sorts (`strcmp`). */
|
|
1064
|
+
const order = (a, b) => (a.id ?? "") < (b.id ?? "") ? -1 : (a.id ?? "") > (b.id ?? "") ? 1 : 0;
|
|
1065
|
+
const on = (x) => x.enabled ?? (x.status === void 0 || x.status === "enabled");
|
|
1066
|
+
/** Same-site targets become root-relative, like every `from`; other hosts stay absolute (as the Bridge does). */
|
|
1067
|
+
function relative(url, origin) {
|
|
1068
|
+
try {
|
|
1069
|
+
const home = new URL(origin);
|
|
1070
|
+
const u = new URL(url, `${origin}/`);
|
|
1071
|
+
if (u.host.toLowerCase() !== home.host.toLowerCase()) return url;
|
|
1072
|
+
return `${u.pathname}${u.search}${u.hash}`;
|
|
1073
|
+
} catch {
|
|
1074
|
+
return url;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Redirection's rules and groups → the rules the site serves and the ones it does not, sorted by id.
|
|
1079
|
+
* A rule's query, case and trailing-slash modes are what Redirection applies to it: its own flags, else the site's
|
|
1080
|
+
* (`options`), else the plugin's defaults. Without `options` (not readable) only a rule's own flags are stated.
|
|
1081
|
+
*/
|
|
1082
|
+
function redirectionRules(items, groups, origin, options) {
|
|
1083
|
+
const site = options ? {
|
|
1084
|
+
...DEFAULTS,
|
|
1085
|
+
...valid(options)
|
|
1086
|
+
} : {};
|
|
1087
|
+
const byId = new Map(groups.map((g) => [g.id, g]));
|
|
1088
|
+
const redirects = [];
|
|
1089
|
+
const excluded = [];
|
|
1090
|
+
for (const item of items) {
|
|
1091
|
+
const group = item.group_id === void 0 ? void 0 : byId.get(item.group_id);
|
|
1092
|
+
const code = Number(item.action_code ?? 301);
|
|
1093
|
+
const data = item.action_data;
|
|
1094
|
+
const target = typeof data === "string" ? data : data?.url ?? "";
|
|
1095
|
+
const own = item.match_data?.source ?? {};
|
|
1096
|
+
const flags = {
|
|
1097
|
+
...site,
|
|
1098
|
+
...valid(own)
|
|
1099
|
+
};
|
|
1100
|
+
const rule = {
|
|
1101
|
+
id: `redirection:${item.id}`,
|
|
1102
|
+
from: item.url ?? "",
|
|
1103
|
+
source: "redirection",
|
|
1104
|
+
match: "url",
|
|
1105
|
+
regex: item.regex === true || own.flag_regex === true
|
|
1106
|
+
};
|
|
1107
|
+
const served = group?.module_id !== void 0 ? MODULES[group.module_id] : void 0;
|
|
1108
|
+
if (served) rule.served_by = served;
|
|
1109
|
+
if (rule.regex) rule.query = "exact";
|
|
1110
|
+
else if (typeof flags.flag_query === "string") rule.query = QUERY[flags.flag_query];
|
|
1111
|
+
if (typeof flags.flag_case === "boolean") rule.case_insensitive = flags.flag_case;
|
|
1112
|
+
if (typeof flags.flag_trailing === "boolean") rule.trailing_slash = flags.flag_trailing ? "ignore" : "exact";
|
|
1113
|
+
const matchType = item.match_type ?? "url";
|
|
1114
|
+
const actionType = item.action_type ?? "url";
|
|
1115
|
+
if (!on(item) || group && !on(group)) excluded.push({
|
|
1116
|
+
...rule,
|
|
1117
|
+
reason: "disabled"
|
|
1118
|
+
});
|
|
1119
|
+
else if (actionType === "error" && GONE.has(code) && matchType === "url") redirects.push({
|
|
1120
|
+
...rule,
|
|
1121
|
+
to: "",
|
|
1122
|
+
status: code
|
|
1123
|
+
});
|
|
1124
|
+
else if (actionType !== "url") excluded.push({
|
|
1125
|
+
...rule,
|
|
1126
|
+
status: code,
|
|
1127
|
+
reason: `not-a-redirect:${actionType}`
|
|
1128
|
+
});
|
|
1129
|
+
else if (matchType !== "url") excluded.push({
|
|
1130
|
+
...rule,
|
|
1131
|
+
match: matchType,
|
|
1132
|
+
status: code,
|
|
1133
|
+
...matchType === "login" ? { condition: data ?? null } : {},
|
|
1134
|
+
reason: `conditional-match:${matchType}`
|
|
1135
|
+
});
|
|
1136
|
+
else {
|
|
1137
|
+
const move = code >= 300 && code < 400;
|
|
1138
|
+
redirects.push({
|
|
1139
|
+
...rule,
|
|
1140
|
+
to: relative(target, origin),
|
|
1141
|
+
status: move ? code : 301,
|
|
1142
|
+
...move ? {} : { status_note: `source status ${code} is not a redirect code; 301 assumed` }
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
redirects: redirects.toSorted(order),
|
|
1148
|
+
excluded: excluded.toSorted(order)
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
//#endregion
|
|
678
1152
|
//#region src/rest-menus.ts
|
|
679
1153
|
const targetOf = (kind, object, id, url, ctx) => {
|
|
680
1154
|
const objectId = id && id > 0 ? id : null;
|
|
@@ -713,6 +1187,12 @@ const targetOf = (kind, object, id, url, ctx) => {
|
|
|
713
1187
|
resolved: false
|
|
714
1188
|
};
|
|
715
1189
|
};
|
|
1190
|
+
/** The public address of the post or term a target names, when it is one this import proved public. */
|
|
1191
|
+
const publicLink = (target, ctx) => {
|
|
1192
|
+
if (!target.resolved || !("id" in target) || !target.id) return void 0;
|
|
1193
|
+
if (target.kind === "post") return ctx.postLink?.(target.id);
|
|
1194
|
+
if (target.kind === "term") return ctx.termLink?.(target.taxonomy, target.id);
|
|
1195
|
+
};
|
|
716
1196
|
/**
|
|
717
1197
|
* Items that are left out: a draft item, or one whose post target is not proven visible. Their children move up
|
|
718
1198
|
* to the nearest kept ancestor, as WordPress's walker shows them.
|
|
@@ -733,12 +1213,35 @@ function hiddenItems(items, drop) {
|
|
|
733
1213
|
lift
|
|
734
1214
|
};
|
|
735
1215
|
}
|
|
1216
|
+
/** A host without case or a leading `www.`: example.com and www.example.com are one site (as the Bridge reads it). */
|
|
1217
|
+
const bareHost = (u) => u.hostname.toLowerCase().replace(/^www\./, "");
|
|
1218
|
+
/**
|
|
1219
|
+
* The post a plain permalink names (`/?page_id=14`, `/?p=14`) on this site, `www.` or not: a custom link typed by
|
|
1220
|
+
* hand carries no post id, but it points at a post all the same, and its label is often that post's title.
|
|
1221
|
+
*/
|
|
1222
|
+
function plainPostId(url, origin) {
|
|
1223
|
+
if (!url || !url.includes("?")) return void 0;
|
|
1224
|
+
try {
|
|
1225
|
+
const u = new URL(url, `${origin}/`);
|
|
1226
|
+
if (bareHost(u) !== bareHost(new URL(origin))) return void 0;
|
|
1227
|
+
const id = Number(u.searchParams.get("page_id") ?? u.searchParams.get("p"));
|
|
1228
|
+
return Number.isSafeInteger(id) && id > 0 ? id : void 0;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
736
1233
|
/** Classic menus and their items; each menu names the theme locations it is assigned to. `dropped` counts left-out items. */
|
|
737
1234
|
function classicMenus(menus, items, ctx, dropped = { count: 0 }) {
|
|
738
1235
|
const out = [];
|
|
739
1236
|
for (const m of [...menus].toSorted((a, b) => a.id - b.id)) {
|
|
740
1237
|
const all = items.filter((i) => i.menus === m.id).toSorted((a, b) => (a.menu_order ?? 0) - (b.menu_order ?? 0) || a.id - b.id);
|
|
741
|
-
const
|
|
1238
|
+
const hidden = (i) => {
|
|
1239
|
+
if ((i.status ?? "publish") !== "publish") return true;
|
|
1240
|
+
if (i.type === "post_type") return !(i.object_id && ctx.postSlug(i.object_id));
|
|
1241
|
+
const id = plainPostId(i.url, ctx.origin);
|
|
1242
|
+
return id !== void 0 && !ctx.postSlug(id);
|
|
1243
|
+
};
|
|
1244
|
+
const { gone, lift } = hiddenItems(all, hidden);
|
|
742
1245
|
dropped.count += gone.size;
|
|
743
1246
|
const own = all.filter((i) => !gone.has(i.id));
|
|
744
1247
|
const ids = new Set(own.map((i) => i.id));
|
|
@@ -747,13 +1250,15 @@ function classicMenus(menus, items, ctx, dropped = { count: 0 }) {
|
|
|
747
1250
|
slug: m.slug ?? "",
|
|
748
1251
|
name: strip(m.name ?? m.slug ?? ""),
|
|
749
1252
|
items: own.map((i) => {
|
|
1253
|
+
const target = targetOf(i.type, i.object, i.object_id, i.url, ctx);
|
|
1254
|
+
const typed = target.kind === "url" ? plainPostId(i.url, ctx.origin) : void 0;
|
|
750
1255
|
const item = {
|
|
751
1256
|
id: i.id,
|
|
752
1257
|
title: i.title?.raw || i.title?.rendered || "",
|
|
753
1258
|
order: i.menu_order ?? 0,
|
|
754
1259
|
parent: lift(i.parent || null),
|
|
755
|
-
url: i.url || null,
|
|
756
|
-
target
|
|
1260
|
+
url: publicLink(target, ctx) ?? (typed !== void 0 ? ctx.postLink?.(typed) : void 0) ?? (i.url || null),
|
|
1261
|
+
target,
|
|
757
1262
|
target_attr: i.target || null,
|
|
758
1263
|
classes: (i.classes ?? []).filter(Boolean),
|
|
759
1264
|
description: i.description ?? "",
|
|
@@ -805,6 +1310,7 @@ function parseBlocks(content) {
|
|
|
805
1310
|
const str = (v) => typeof v === "string" && v ? v : void 0;
|
|
806
1311
|
const absolute = (url, origin) => {
|
|
807
1312
|
if (!url) return void 0;
|
|
1313
|
+
if (url.startsWith("#")) return url;
|
|
808
1314
|
try {
|
|
809
1315
|
return new URL(url, `${origin}/`).href;
|
|
810
1316
|
} catch {
|
|
@@ -822,17 +1328,22 @@ const KIND = {
|
|
|
822
1328
|
* published pages it lists; a home link points at `/`. Other blocks (search, social icons, spacers) are not links.
|
|
823
1329
|
*/
|
|
824
1330
|
function navigationItems(content, ctx, nextId, dropped = { count: 0 }) {
|
|
1331
|
+
return blockItems(parseBlocks(content), ctx, nextId, dropped);
|
|
1332
|
+
}
|
|
1333
|
+
/** Menu items of navigation blocks already parsed (a `wp_navigation` body, or an inline navigation's links). */
|
|
1334
|
+
function blockItems(tree, ctx, nextId, dropped) {
|
|
825
1335
|
const items = [];
|
|
826
1336
|
const push = (b, parent) => {
|
|
827
1337
|
const id = nextId();
|
|
828
1338
|
const url = absolute(b.url, ctx.origin);
|
|
1339
|
+
const target = targetOf(b.kind, b.object, b.id, url, ctx);
|
|
829
1340
|
items.push({
|
|
830
1341
|
id,
|
|
831
1342
|
title: b.title,
|
|
832
1343
|
order: items.length + 1,
|
|
833
1344
|
parent,
|
|
834
|
-
url: url ?? null,
|
|
835
|
-
target
|
|
1345
|
+
url: publicLink(target, ctx) ?? (b.typed !== void 0 ? ctx.postLink?.(b.typed) : void 0) ?? url ?? null,
|
|
1346
|
+
target,
|
|
836
1347
|
target_attr: b.newTab ? "_blank" : null,
|
|
837
1348
|
classes: (b.className ?? "").split(/\s+/).filter(Boolean),
|
|
838
1349
|
description: b.description ?? "",
|
|
@@ -859,7 +1370,8 @@ function navigationItems(content, ctx, nextId, dropped = { count: 0 }) {
|
|
|
859
1370
|
if (b.name === "core/navigation-link" || b.name === "core/navigation-submenu") {
|
|
860
1371
|
const kind = KIND[str(a.kind) ?? ""] ?? (str(a.type) === "category" || str(a.type) === "tag" || str(a.type) === "post_tag" ? "taxonomy" : a.id ? "post_type" : "custom");
|
|
861
1372
|
const object = kind === "taxonomy" && str(a.type) === "tag" ? "post_tag" : str(a.type);
|
|
862
|
-
|
|
1373
|
+
const postId = kind === "post_type" && typeof a.id === "number" ? a.id : kind === "custom" ? plainPostId(str(a.url), ctx.origin) : void 0;
|
|
1374
|
+
if (postId !== void 0 && !ctx.postSlug(postId)) {
|
|
863
1375
|
dropped.count++;
|
|
864
1376
|
walk(b.children, parent);
|
|
865
1377
|
continue;
|
|
@@ -870,6 +1382,7 @@ function navigationItems(content, ctx, nextId, dropped = { count: 0 }) {
|
|
|
870
1382
|
kind,
|
|
871
1383
|
object,
|
|
872
1384
|
id: typeof a.id === "number" ? a.id : void 0,
|
|
1385
|
+
typed: kind === "custom" ? postId : void 0,
|
|
873
1386
|
newTab: a.opensInNewTab === true,
|
|
874
1387
|
className: str(a.className),
|
|
875
1388
|
description: str(a.description)
|
|
@@ -884,15 +1397,94 @@ function navigationItems(content, ctx, nextId, dropped = { count: 0 }) {
|
|
|
884
1397
|
else walk(b.children, parent);
|
|
885
1398
|
}
|
|
886
1399
|
};
|
|
887
|
-
walk(
|
|
1400
|
+
walk(tree, null);
|
|
888
1401
|
return items;
|
|
889
1402
|
}
|
|
1403
|
+
/** Template-part slugs the templates use, at any depth (a part inside a group, a part inside a part is not followed). */
|
|
1404
|
+
function partSlugs(blocks, out = /* @__PURE__ */ new Set()) {
|
|
1405
|
+
for (const b of blocks) {
|
|
1406
|
+
if (b.name === "core/template-part" && typeof b.attrs.slug === "string") out.add(b.attrs.slug);
|
|
1407
|
+
partSlugs(b.children, out);
|
|
1408
|
+
}
|
|
1409
|
+
return out;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* The template parts a page can show: those a template references. Without the templates (not readable), the
|
|
1413
|
+
* part named after its area (`header`, `footer`) — what a theme's templates use unless they say otherwise.
|
|
1414
|
+
* Parts without a slug (older callers) all count.
|
|
1415
|
+
*/
|
|
1416
|
+
function usedParts(parts, templates = []) {
|
|
1417
|
+
const used = /* @__PURE__ */ new Set();
|
|
1418
|
+
for (const t of templates) if (t.content?.raw) partSlugs(parseBlocks(t.content.raw), used);
|
|
1419
|
+
return parts.filter((p) => !p.slug || (used.size ? used.has(p.slug) : p.slug === p.area));
|
|
1420
|
+
}
|
|
1421
|
+
const AREA_ORDER = ["header", "footer"];
|
|
1422
|
+
const areaOf = (p) => p.area && p.area !== "uncategorized" ? p.area : void 0;
|
|
1423
|
+
/**
|
|
1424
|
+
* A part's blocks with the parts it holds in their place (a header holding a `navigation` part), each part once:
|
|
1425
|
+
* what the part shows, in its area.
|
|
1426
|
+
*/
|
|
1427
|
+
function partTree(part, all) {
|
|
1428
|
+
const bySlug = new Map(all.filter((p) => p.slug).map((p) => [p.slug, p]));
|
|
1429
|
+
const seen = new Set(part.slug ? [part.slug] : []);
|
|
1430
|
+
const expand = (blocks) => blocks.flatMap((b) => {
|
|
1431
|
+
if (b.name === "core/template-part") {
|
|
1432
|
+
const inner = typeof b.attrs.slug === "string" ? bySlug.get(b.attrs.slug) : void 0;
|
|
1433
|
+
if (!inner?.content?.raw || seen.has(inner.slug)) return [];
|
|
1434
|
+
seen.add(inner.slug);
|
|
1435
|
+
return expand(parseBlocks(inner.content.raw));
|
|
1436
|
+
}
|
|
1437
|
+
return [{
|
|
1438
|
+
...b,
|
|
1439
|
+
children: expand(b.children)
|
|
1440
|
+
}];
|
|
1441
|
+
});
|
|
1442
|
+
return expand(parseBlocks(part.content?.raw ?? ""));
|
|
1443
|
+
}
|
|
1444
|
+
const titleCase = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1445
|
+
const slugOf = (s) => s.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1446
|
+
/**
|
|
1447
|
+
* Inline navigations of the used template parts → RawMenu[], in area order (header, footer, others) and document
|
|
1448
|
+
* order within a part. A part's single inline navigation is named after its area (`Footer navigation`), several
|
|
1449
|
+
* are numbered; a navigation's own `ariaLabel` wins. Links keep their order and nesting; a link to content not
|
|
1450
|
+
* proven public is left out, as in every other menu.
|
|
1451
|
+
*/
|
|
1452
|
+
function inlineMenus(parts, ctx, taken, nextId, dropped = { count: 0 }, all = parts) {
|
|
1453
|
+
const out = [];
|
|
1454
|
+
const ordered = parts.filter((p) => areaOf(p) && p.content?.raw).toSorted((a, b) => (AREA_ORDER.indexOf(areaOf(a)) + 1 || 99) - (AREA_ORDER.indexOf(areaOf(b)) + 1 || 99));
|
|
1455
|
+
for (const p of ordered) {
|
|
1456
|
+
const area = areaOf(p);
|
|
1457
|
+
const navs = [];
|
|
1458
|
+
const find = (blocks) => {
|
|
1459
|
+
for (const b of blocks) if (b.name === "core/navigation") {
|
|
1460
|
+
if (typeof b.attrs.ref !== "number" && b.children.length) navs.push(b);
|
|
1461
|
+
} else find(b.children);
|
|
1462
|
+
};
|
|
1463
|
+
find(partTree(p, all));
|
|
1464
|
+
navs.forEach((nav, i) => {
|
|
1465
|
+
const name = strip(nav.attrs.ariaLabel) || `${titleCase(area)} navigation${navs.length > 1 ? ` ${i + 1}` : ""}`;
|
|
1466
|
+
const base = slugOf(name) || `${area}-navigation`;
|
|
1467
|
+
let slug = base;
|
|
1468
|
+
for (let k = 2; taken.has(slug); k++) slug = `${base}-${k}`;
|
|
1469
|
+
taken.add(slug);
|
|
1470
|
+
const items = blockItems(nav.children, ctx, nextId, dropped);
|
|
1471
|
+
if (items.length) out.push({
|
|
1472
|
+
id: nextId(),
|
|
1473
|
+
slug,
|
|
1474
|
+
name,
|
|
1475
|
+
items,
|
|
1476
|
+
locations: [area]
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return out;
|
|
1481
|
+
}
|
|
890
1482
|
/**
|
|
891
1483
|
* Where each navigation is shown: the template parts (`header`, `footer`) whose navigation block
|
|
892
1484
|
* refers to it. An empty navigation block without a `ref` shows the fallback — the most recent published one;
|
|
893
1485
|
* one with links of its own shows those (an inline menu, not a `wp_navigation` post).
|
|
894
1486
|
*/
|
|
895
|
-
function navigationLocations(parts, navigations) {
|
|
1487
|
+
function navigationLocations(parts, navigations, all = parts) {
|
|
896
1488
|
const out = /* @__PURE__ */ new Map();
|
|
897
1489
|
const fallback = [...navigations].toSorted((a, b) => (b.date_gmt ?? "").localeCompare(a.date_gmt ?? "") || b.id - a.id)[0]?.id;
|
|
898
1490
|
const find = (blocks, area) => {
|
|
@@ -906,15 +1498,19 @@ function navigationLocations(parts, navigations) {
|
|
|
906
1498
|
}
|
|
907
1499
|
};
|
|
908
1500
|
for (const p of parts) {
|
|
909
|
-
const area = p
|
|
910
|
-
if (area && p.content?.raw) find(
|
|
1501
|
+
const area = areaOf(p);
|
|
1502
|
+
if (area && p.content?.raw) find(partTree(p, all), area);
|
|
911
1503
|
}
|
|
912
1504
|
return new Map([...out].map(([id, set]) => [id, [...set].toSorted()]));
|
|
913
1505
|
}
|
|
914
|
-
/**
|
|
915
|
-
|
|
1506
|
+
/**
|
|
1507
|
+
* Published `wp_navigation` posts, then the inline navigations of the used template parts → RawMenu[], slugs kept
|
|
1508
|
+
* unique against the classic menus already read. `templates` says which parts are used (see `usedParts`).
|
|
1509
|
+
*/
|
|
1510
|
+
function blockMenus(navigations, allParts, ctx, taken, dropped = { count: 0 }, templates = []) {
|
|
1511
|
+
const parts = usedParts(allParts, templates);
|
|
916
1512
|
const published = navigations.filter((n) => (n.status ?? "publish") === "publish").toSorted((a, b) => a.id - b.id);
|
|
917
|
-
const locations = navigationLocations(parts, published);
|
|
1513
|
+
const locations = navigationLocations(parts, published, allParts);
|
|
918
1514
|
let next = 0;
|
|
919
1515
|
const out = [];
|
|
920
1516
|
for (const n of published) {
|
|
@@ -931,6 +1527,7 @@ function blockMenus(navigations, parts, ctx, taken, dropped = { count: 0 }) {
|
|
|
931
1527
|
if (where?.length) menu.locations = where;
|
|
932
1528
|
out.push(menu);
|
|
933
1529
|
}
|
|
1530
|
+
out.push(...inlineMenus(parts, ctx, taken, () => -++next, dropped, allParts));
|
|
934
1531
|
return out;
|
|
935
1532
|
}
|
|
936
1533
|
//#endregion
|
|
@@ -952,8 +1549,41 @@ const DENIED = new Set([
|
|
|
952
1549
|
401,
|
|
953
1550
|
403
|
|
954
1551
|
]);
|
|
1552
|
+
/** REST taxonomies that hold no content: menus, block-pattern categories, multilingual bookkeeping. */
|
|
1553
|
+
const NOT_CONTENT_TAXONOMIES = new Set([
|
|
1554
|
+
"category",
|
|
1555
|
+
"post_tag",
|
|
1556
|
+
"nav_menu",
|
|
1557
|
+
"wp_pattern_category",
|
|
1558
|
+
"post_format",
|
|
1559
|
+
"language",
|
|
1560
|
+
"post_translations",
|
|
1561
|
+
"term_language",
|
|
1562
|
+
"term_translations"
|
|
1563
|
+
]);
|
|
955
1564
|
/** A comment listing's name in `credential.fell_back`: `comments` (approved) or `comments:hold`. */
|
|
956
1565
|
const commentKey = (status) => status === "approve" ? "comments" : `comments:${status}`;
|
|
1566
|
+
/**
|
|
1567
|
+
* ACF fields of one REST post. `<name>_source` (SCF) states each field's type and label; a secret — a
|
|
1568
|
+
* `password` field, or, when no type is stated, a field named like one — is never read. ACF answers `[]`
|
|
1569
|
+
* for a post with no field group in REST.
|
|
1570
|
+
*/
|
|
1571
|
+
function acfOf(acf) {
|
|
1572
|
+
if (!acf || Array.isArray(acf) || typeof acf !== "object") return {};
|
|
1573
|
+
const out = {};
|
|
1574
|
+
for (const [name, value] of Object.entries(acf)) {
|
|
1575
|
+
if (name.endsWith("_source") && name.slice(0, -7) in acf) continue;
|
|
1576
|
+
const source = acf[`${name}_source`];
|
|
1577
|
+
const type = typeof source?.type === "string" ? source.type : void 0;
|
|
1578
|
+
if (acfIsSecret(name, type)) continue;
|
|
1579
|
+
out[name] = {
|
|
1580
|
+
value: acfScrub(value),
|
|
1581
|
+
...type ? { type } : {},
|
|
1582
|
+
...typeof source?.label === "string" && source.label ? { label: source.label } : {}
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
return Object.keys(out).length ? { acf: out } : {};
|
|
1586
|
+
}
|
|
957
1587
|
/** What a visitor can open: published, not behind a post password. */
|
|
958
1588
|
const visible = (p) => p.status === "publish" && !p.password;
|
|
959
1589
|
const positiveInteger = (name, value) => {
|
|
@@ -1063,10 +1693,15 @@ async function fetchRestRawIR(options) {
|
|
|
1063
1693
|
return null;
|
|
1064
1694
|
}
|
|
1065
1695
|
});
|
|
1066
|
-
|
|
1696
|
+
let typesResp = await doFetch(`${origin}/wp-json/wp/v2/types${options.auth && !rejected ? "?context=edit" : ""}`, { headers });
|
|
1697
|
+
if (!typesResp.ok && options.auth && !rejected) {
|
|
1698
|
+
await typesResp.body?.cancel();
|
|
1699
|
+
typesResp = await doFetch(`${origin}/wp-json/wp/v2/types`, { headers: anonymous });
|
|
1700
|
+
}
|
|
1067
1701
|
const types = typesResp.ok ? await typesResp.json() : {};
|
|
1068
1702
|
if (!typesResp.ok) warnings.push(`types: HTTP ${typesResp.status} — importing posts and pages only`);
|
|
1069
1703
|
const postTypes = Object.values(types).filter((t) => t.rest_base && !SKIP_TYPES.test(t.slug) && t.slug !== "attachment");
|
|
1704
|
+
const notViewable = new Set(postTypes.filter((t) => t.viewable === false).map((t) => t.slug));
|
|
1070
1705
|
const bases = postTypes.length ? postTypes.map((t) => ({
|
|
1071
1706
|
slug: t.slug,
|
|
1072
1707
|
base: t.rest_base
|
|
@@ -1090,24 +1725,52 @@ async function fetchRestRawIR(options) {
|
|
|
1090
1725
|
for (const c of lists.flat()) if (!byId.has(c.id)) byId.set(c.id, c);
|
|
1091
1726
|
return [...byId.values()];
|
|
1092
1727
|
};
|
|
1093
|
-
const
|
|
1728
|
+
const taxResp = await schedule(async () => {
|
|
1729
|
+
try {
|
|
1730
|
+
const r = await doFetch(`${origin}/wp-json/wp/v2/taxonomies`, { headers });
|
|
1731
|
+
if (!r.ok) {
|
|
1732
|
+
await r.body?.cancel();
|
|
1733
|
+
return {};
|
|
1734
|
+
}
|
|
1735
|
+
return await r.json();
|
|
1736
|
+
} catch {
|
|
1737
|
+
return {};
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
const extraTaxonomies = Object.values(taxResp && typeof taxResp === "object" ? taxResp : {}).filter((t) => t.rest_base && /^[\w-]+$/.test(t.rest_base) && !NOT_CONTENT_TAXONOMIES.has(t.slug)).map((t) => ({
|
|
1741
|
+
taxonomy: t.slug,
|
|
1742
|
+
base: t.rest_base
|
|
1743
|
+
})).toSorted((a, b) => a.taxonomy.localeCompare(b.taxonomy));
|
|
1744
|
+
const [categories, tags, users, media, restComments, ...lists] = await Promise.all([
|
|
1094
1745
|
getAll("categories"),
|
|
1095
1746
|
getAll("tags"),
|
|
1096
1747
|
getAll("users"),
|
|
1097
1748
|
getAll("media"),
|
|
1098
1749
|
commentListing(),
|
|
1750
|
+
...extraTaxonomies.map((t) => getAll(t.base)),
|
|
1099
1751
|
...bases.map((b) => postListing(b.base))
|
|
1100
1752
|
]);
|
|
1753
|
+
const extraTerms = lists.slice(0, extraTaxonomies.length);
|
|
1754
|
+
const postLists = lists.slice(extraTaxonomies.length);
|
|
1101
1755
|
const termsById = /* @__PURE__ */ new Map();
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1756
|
+
/** A term's public address, for menu items that name it; RawTerm carries none. */
|
|
1757
|
+
const termLinks = /* @__PURE__ */ new Map();
|
|
1758
|
+
for (const [tax, list] of [
|
|
1759
|
+
["category", categories],
|
|
1760
|
+
["post_tag", tags],
|
|
1761
|
+
...extraTaxonomies.map((t, i) => [t.taxonomy, extraTerms[i] ?? []])
|
|
1762
|
+
]) for (const t of list) {
|
|
1763
|
+
if (t.link) termLinks.set(t.id, t.link);
|
|
1764
|
+
termsById.set(t.id, {
|
|
1765
|
+
id: t.id,
|
|
1766
|
+
taxonomy: tax,
|
|
1767
|
+
slug: t.slug,
|
|
1768
|
+
name: strip(t.name),
|
|
1769
|
+
parent: t.parent ? list.find((x) => x.id === t.parent)?.slug ?? null : null,
|
|
1770
|
+
parent_resolved: t.parent ? list.some((x) => x.id === t.parent) : null,
|
|
1771
|
+
description: t.description ?? ""
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1111
1774
|
const userById = new Map(users.map((u) => [u.id, u]));
|
|
1112
1775
|
const posts = [];
|
|
1113
1776
|
const languagePairs = [];
|
|
@@ -1131,7 +1794,15 @@ async function fetchRestRawIR(options) {
|
|
|
1131
1794
|
});
|
|
1132
1795
|
};
|
|
1133
1796
|
for (const [i, base] of bases.entries()) for (const p of postLists[i] ?? []) {
|
|
1134
|
-
const
|
|
1797
|
+
const custom = extraTaxonomies.flatMap((t) => {
|
|
1798
|
+
const ids = p[t.base];
|
|
1799
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "number") : [];
|
|
1800
|
+
});
|
|
1801
|
+
const termRefs = [
|
|
1802
|
+
...p.categories ?? [],
|
|
1803
|
+
...p.tags ?? [],
|
|
1804
|
+
...custom
|
|
1805
|
+
].map((id) => {
|
|
1135
1806
|
const t = termsById.get(id);
|
|
1136
1807
|
return t ? {
|
|
1137
1808
|
taxonomy: t.taxonomy,
|
|
@@ -1157,7 +1828,7 @@ async function fetchRestRawIR(options) {
|
|
|
1157
1828
|
status: p.status ?? "publish",
|
|
1158
1829
|
slug: p.slug,
|
|
1159
1830
|
title: p.title?.rendered ?? "",
|
|
1160
|
-
link: p.link ?? null,
|
|
1831
|
+
link: notViewable.has(base.slug) ? null : p.link ?? null,
|
|
1161
1832
|
guid: null,
|
|
1162
1833
|
author,
|
|
1163
1834
|
date: iso(p.date_gmt),
|
|
@@ -1171,7 +1842,8 @@ async function fetchRestRawIR(options) {
|
|
|
1171
1842
|
comment_status: p.comment_status ?? null,
|
|
1172
1843
|
ping_status: p.ping_status ?? null,
|
|
1173
1844
|
terms: termRefs,
|
|
1174
|
-
meta
|
|
1845
|
+
meta,
|
|
1846
|
+
...acfOf(p.acf)
|
|
1175
1847
|
});
|
|
1176
1848
|
}
|
|
1177
1849
|
const attachments = media.map((m) => ({
|
|
@@ -1186,9 +1858,11 @@ async function fetchRestRawIR(options) {
|
|
|
1186
1858
|
mime: m.mime_type ?? null,
|
|
1187
1859
|
parent: m.post ?? null,
|
|
1188
1860
|
parent_resolved: m.post ? posts.some((p) => p.id === m.post) : null,
|
|
1861
|
+
link: !m.post || posts.some((p) => p.id === m.post && visible(p)) ? m.link || null : null,
|
|
1189
1862
|
date: iso(m.date_gmt)
|
|
1190
1863
|
}));
|
|
1191
1864
|
const gaps = [];
|
|
1865
|
+
if (postLists.some((list) => list.some((p) => p.acf !== void 0))) gaps.push("acf_partial");
|
|
1192
1866
|
let menus = [];
|
|
1193
1867
|
if (!authed) gaps.push("menus_require_auth");
|
|
1194
1868
|
else {
|
|
@@ -1227,11 +1901,12 @@ async function fetchRestRawIR(options) {
|
|
|
1227
1901
|
status: first.status
|
|
1228
1902
|
};
|
|
1229
1903
|
};
|
|
1230
|
-
const [classic, items, navs, parts] = await Promise.all([
|
|
1904
|
+
const [classic, items, navs, parts, templates] = await Promise.all([
|
|
1231
1905
|
listing("menus?context=edit"),
|
|
1232
1906
|
listing("menu-items?context=edit"),
|
|
1233
1907
|
listing("navigation?context=edit&status=publish"),
|
|
1234
|
-
listing("template-parts?context=edit")
|
|
1908
|
+
listing("template-parts?context=edit"),
|
|
1909
|
+
listing("templates?context=edit&_fields=slug,content")
|
|
1235
1910
|
]);
|
|
1236
1911
|
const denied = [
|
|
1237
1912
|
classic,
|
|
@@ -1248,6 +1923,7 @@ async function fetchRestRawIR(options) {
|
|
|
1248
1923
|
["navigation", navs]
|
|
1249
1924
|
]) if (!l.status || l.status >= 400 && !DENIED.has(l.status) && l.status !== 404) warnings.push(`${name}: HTTP ${l.status} — skipped`);
|
|
1250
1925
|
const slugOf = new Map(posts.filter(visible).map((p) => [p.id, p.slug]));
|
|
1926
|
+
const linkOf = new Map(posts.filter(visible).flatMap((p) => p.link ? [[p.id, p.link]] : []));
|
|
1251
1927
|
const ctx = {
|
|
1252
1928
|
origin,
|
|
1253
1929
|
postSlug: (id) => slugOf.get(id),
|
|
@@ -1255,6 +1931,8 @@ async function fetchRestRawIR(options) {
|
|
|
1255
1931
|
const t = termsById.get(id);
|
|
1256
1932
|
return t && t.taxonomy === taxonomy ? t.slug : void 0;
|
|
1257
1933
|
},
|
|
1934
|
+
postLink: (id) => linkOf.get(id),
|
|
1935
|
+
termLink: (taxonomy, id) => termsById.get(id)?.taxonomy === taxonomy ? termLinks.get(id) : void 0,
|
|
1258
1936
|
pages: posts.filter((p) => p.type === "page" && p.status === "publish" && !p.password).map((p) => ({
|
|
1259
1937
|
id: p.id,
|
|
1260
1938
|
parent: p.parent ?? null,
|
|
@@ -1266,7 +1944,7 @@ async function fetchRestRawIR(options) {
|
|
|
1266
1944
|
};
|
|
1267
1945
|
const dropped = { count: 0 };
|
|
1268
1946
|
menus = classicMenus(DENIED.has(classic.status) ? [] : classic.items, DENIED.has(items.status) ? [] : items.items, ctx, dropped);
|
|
1269
|
-
menus.push(...blockMenus(DENIED.has(navs.status) ? [] : navs.items, parts.status < 400 ? parts.items : [], ctx, new Set(menus.map((m) => m.slug)), dropped));
|
|
1947
|
+
menus.push(...blockMenus(DENIED.has(navs.status) ? [] : navs.items, parts.status < 400 ? parts.items : [], ctx, new Set(menus.map((m) => m.slug)), dropped, templates.status < 400 ? templates.items : []));
|
|
1270
1948
|
if (dropped.count) warnings.push(`menus: ${dropped.count} item(s) are drafts or point at content not proven public (unpublished, password-protected, or not read by this import) — left out`);
|
|
1271
1949
|
}
|
|
1272
1950
|
const postIds = new Set(posts.map((p) => p.id));
|
|
@@ -1288,6 +1966,62 @@ async function fetchRestRawIR(options) {
|
|
|
1288
1966
|
user_id: c.author || null
|
|
1289
1967
|
}));
|
|
1290
1968
|
for (const c of comments) if (!postIds.has(c.post)) warnings.push(`comment ${c.id}: post ${c.post} not in fetched set`);
|
|
1969
|
+
let redirects = [];
|
|
1970
|
+
let redirectsExcluded = [];
|
|
1971
|
+
gaps.push("redirects_partial");
|
|
1972
|
+
const namespaces = Array.isArray(about?.namespaces) ? about.namespaces : void 0;
|
|
1973
|
+
const redirection = namespaces ? namespaces.includes("redirection/v1") : void 0;
|
|
1974
|
+
if (redirection && !authed) gaps.push("redirects_require_auth");
|
|
1975
|
+
else if (authed && redirection !== false) {
|
|
1976
|
+
const all = async (path) => {
|
|
1977
|
+
const items = [];
|
|
1978
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1979
|
+
for (let page = 0; page < (maxPages ?? 500); page++) {
|
|
1980
|
+
const r = await schedule(() => doFetch(`${origin}/wp-json/redirection/v1/${path}?per_page=200&page=${page}`, { headers }));
|
|
1981
|
+
if (!r.ok) {
|
|
1982
|
+
await r.body?.cancel();
|
|
1983
|
+
return {
|
|
1984
|
+
items,
|
|
1985
|
+
status: r.status
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
const body = await r.json();
|
|
1989
|
+
const fresh = (Array.isArray(body.items) ? body.items : []).filter((x) => !seen.has(x.id));
|
|
1990
|
+
for (const x of fresh) {
|
|
1991
|
+
seen.add(x.id);
|
|
1992
|
+
items.push(x);
|
|
1993
|
+
}
|
|
1994
|
+
if (!fresh.length || items.length >= Number(body.total ?? 0)) break;
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
items,
|
|
1998
|
+
status: 200
|
|
1999
|
+
};
|
|
2000
|
+
};
|
|
2001
|
+
const settings = async () => {
|
|
2002
|
+
try {
|
|
2003
|
+
const r = await schedule(() => doFetch(`${origin}/wp-json/redirection/v1/setting`, { headers }));
|
|
2004
|
+
if (!r.ok) {
|
|
2005
|
+
await r.body?.cancel();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const body = await r.json();
|
|
2009
|
+
return body?.settings && typeof body.settings === "object" ? body.settings : void 0;
|
|
2010
|
+
} catch {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
const [rules, groups, siteFlags] = await Promise.all([
|
|
2015
|
+
all("redirect"),
|
|
2016
|
+
all("group"),
|
|
2017
|
+
settings()
|
|
2018
|
+
]);
|
|
2019
|
+
if (DENIED.has(rules.status)) {
|
|
2020
|
+
gaps.push("redirects_require_auth");
|
|
2021
|
+
warnings.push(`redirects: HTTP ${rules.status} with the credential — the user may not manage Redirection; its rules not read`);
|
|
2022
|
+
} else if (rules.status === 200) ({redirects, excluded: redirectsExcluded} = redirectionRules(rules.items, groups.status === 200 ? groups.items : [], origin, siteFlags));
|
|
2023
|
+
else if (redirection) warnings.push(`redirects: HTTP ${rules.status} — Redirection's rules not read`);
|
|
2024
|
+
}
|
|
1291
2025
|
return {
|
|
1292
2026
|
raw: {
|
|
1293
2027
|
version: MIGRATION_CONTRACT_VERSION,
|
|
@@ -1300,7 +2034,9 @@ async function fetchRestRawIR(options) {
|
|
|
1300
2034
|
...typeof about?.name === "string" && strip(about.name) ? { title: strip(about.name) } : {},
|
|
1301
2035
|
...typeof about?.description === "string" && strip(about.description) ? { description: strip(about.description) } : {},
|
|
1302
2036
|
...typeof about?.url === "string" && about.url ? { base_site_url: about.url } : {},
|
|
1303
|
-
...typeof about?.home === "string" && about.home ? { base_blog_url: about.home } : {}
|
|
2037
|
+
...typeof about?.home === "string" && about.home ? { base_blog_url: about.home } : {},
|
|
2038
|
+
...typeof about?.timezone_string === "string" && about.timezone_string ? { timezone: about.timezone_string } : {},
|
|
2039
|
+
...about?.gmt_offset !== void 0 && about.gmt_offset !== null && about.gmt_offset !== "" && Number.isFinite(Number(about.gmt_offset)) ? { gmt_offset: Number(about.gmt_offset) } : {}
|
|
1304
2040
|
},
|
|
1305
2041
|
authors: users.map((u) => ({
|
|
1306
2042
|
id: u.id,
|
|
@@ -1313,6 +2049,8 @@ async function fetchRestRawIR(options) {
|
|
|
1313
2049
|
attachments,
|
|
1314
2050
|
comments,
|
|
1315
2051
|
...menus.length ? { menus } : {},
|
|
2052
|
+
...redirects.length ? { redirects } : {},
|
|
2053
|
+
...redirectsExcluded.length ? { redirects_excluded: redirectsExcluded } : {},
|
|
1316
2054
|
...languagePairs.length ? { language_pairs: languagePairs } : {}
|
|
1317
2055
|
},
|
|
1318
2056
|
warnings,
|
|
@@ -1355,6 +2093,8 @@ function relationOver(targets) {
|
|
|
1355
2093
|
ref: (entry) => single ? entry.ref : entry
|
|
1356
2094
|
};
|
|
1357
2095
|
}
|
|
2096
|
+
/** Post, term, user or attachment ids of an ACF reference value: ids, numeric strings or `{ ID }` objects. */
|
|
2097
|
+
const idsOf = (v) => (Array.isArray(v) ? v : [v]).map((x) => typeof x === "number" ? x : typeof x === "string" && /^\d+$/.test(x) ? Number(x) : x && typeof x === "object" ? Number(x.ID ?? x.id ?? x.term_id) : NaN).filter((n) => Number.isSafeInteger(n) && n > 0);
|
|
1358
2098
|
function rawToContentrain(raw, opts) {
|
|
1359
2099
|
const updatedBy = opts?.updatedBy ?? "@contentrain/wp-import";
|
|
1360
2100
|
const locale = normLocale(raw.site.language) || "en";
|
|
@@ -1386,12 +2126,15 @@ function rawToContentrain(raw, opts) {
|
|
|
1386
2126
|
title_fallback: 0,
|
|
1387
2127
|
meta_fields: {},
|
|
1388
2128
|
acf_fields: {},
|
|
2129
|
+
acf_outside_choices: {},
|
|
2130
|
+
acf_datetime_unzoned: 0,
|
|
1389
2131
|
skipped_types: [],
|
|
1390
2132
|
dropped_relations: 0,
|
|
1391
2133
|
models: {},
|
|
1392
2134
|
locales,
|
|
1393
2135
|
translation_groups: translationGroups,
|
|
1394
|
-
password_protected_drafts: 0
|
|
2136
|
+
password_protected_drafts: 0,
|
|
2137
|
+
site_title_missing: false
|
|
1395
2138
|
};
|
|
1396
2139
|
const importMeta = (status, extra = {}) => ({
|
|
1397
2140
|
status,
|
|
@@ -1449,6 +2192,100 @@ function rawToContentrain(raw, opts) {
|
|
|
1449
2192
|
});
|
|
1450
2193
|
}
|
|
1451
2194
|
const mediaRef = (id) => raw.attachments.some((a) => a.id === id) ? hexId(`media:${id}`) : null;
|
|
2195
|
+
const termById = new Map(raw.terms.map((t) => [t.id, t]));
|
|
2196
|
+
const authorById = new Map(raw.authors.filter((a) => a.id !== null).map((a) => [a.id, a]));
|
|
2197
|
+
const refOf = (kind, id) => {
|
|
2198
|
+
if (kind === "post") return postEntry.get(id) ?? null;
|
|
2199
|
+
if (kind === "media") {
|
|
2200
|
+
const m = mediaRef(id);
|
|
2201
|
+
return m ? {
|
|
2202
|
+
model: "media",
|
|
2203
|
+
ref: m
|
|
2204
|
+
} : null;
|
|
2205
|
+
}
|
|
2206
|
+
if (kind === "user") {
|
|
2207
|
+
const a = authorById.get(id);
|
|
2208
|
+
return a && models.authors ? {
|
|
2209
|
+
model: "authors",
|
|
2210
|
+
ref: authorId(slugify(a.login))
|
|
2211
|
+
} : null;
|
|
2212
|
+
}
|
|
2213
|
+
if (kind === "term") {
|
|
2214
|
+
const t = termById.get(id);
|
|
2215
|
+
return t && models[taxModelId(t.taxonomy)] ? {
|
|
2216
|
+
model: taxModelId(t.taxonomy),
|
|
2217
|
+
ref: termId(t.taxonomy, t.slug)
|
|
2218
|
+
} : null;
|
|
2219
|
+
}
|
|
2220
|
+
return null;
|
|
2221
|
+
};
|
|
2222
|
+
/** Every ACF field on these posts: its definition, or — for a reference — its kind and target models. */
|
|
2223
|
+
const acfPlan = (items, taken) => {
|
|
2224
|
+
const out = /* @__PURE__ */ new Map();
|
|
2225
|
+
for (const p of items) for (const [k, acf] of Object.entries(p.acf ?? {})) {
|
|
2226
|
+
if (taken(k) || acf.value === null || acf.value === void 0 || acf.value === "" || acf.value === false && acf.type !== "true_false") continue;
|
|
2227
|
+
const kind = acf.type ? ACF_REFERENCE_TYPES[acf.type] : void 0;
|
|
2228
|
+
if (kind === "address") {
|
|
2229
|
+
const def = Array.isArray(acf.value) ? {
|
|
2230
|
+
type: "array",
|
|
2231
|
+
items: "url"
|
|
2232
|
+
} : { type: "url" };
|
|
2233
|
+
const cur = out.get(k);
|
|
2234
|
+
out.set(k, {
|
|
2235
|
+
def: cur?.def ?? {
|
|
2236
|
+
...def,
|
|
2237
|
+
...acf.label ? { label: strip(acf.label) } : {}
|
|
2238
|
+
},
|
|
2239
|
+
kind,
|
|
2240
|
+
multiple: false,
|
|
2241
|
+
models: /* @__PURE__ */ new Set()
|
|
2242
|
+
});
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (kind) {
|
|
2246
|
+
const cur = out.get(k) ?? {
|
|
2247
|
+
kind,
|
|
2248
|
+
multiple: acf.type === "relationship" || acf.type === "gallery",
|
|
2249
|
+
models: /* @__PURE__ */ new Set()
|
|
2250
|
+
};
|
|
2251
|
+
if (Array.isArray(acf.value)) cur.multiple = true;
|
|
2252
|
+
for (const id of idsOf(acf.value)) {
|
|
2253
|
+
const r = refOf(kind, id);
|
|
2254
|
+
if (r) cur.models.add(r.model);
|
|
2255
|
+
}
|
|
2256
|
+
out.set(k, cur);
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const def = acfFieldDef(k, acfScrub(acf.value), {
|
|
2260
|
+
type: acf.type,
|
|
2261
|
+
label: acf.label
|
|
2262
|
+
});
|
|
2263
|
+
if (!def) continue;
|
|
2264
|
+
const cur = out.get(k);
|
|
2265
|
+
out.set(k, {
|
|
2266
|
+
def: cur?.def ? mergeFieldDef(cur.def, def) : def,
|
|
2267
|
+
multiple: false,
|
|
2268
|
+
models: /* @__PURE__ */ new Set()
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
for (const [k, plan] of out) if (!plan.def && !plan.models.size) out.delete(k);
|
|
2272
|
+
return out;
|
|
2273
|
+
};
|
|
2274
|
+
const acfRefs = (plan, value) => {
|
|
2275
|
+
const refs = idsOf(value).map((id) => refOf(plan.kind, id)).filter((r) => !!r);
|
|
2276
|
+
const missing = idsOf(value).length - refs.length;
|
|
2277
|
+
if (missing > 0) report.dropped_relations += missing;
|
|
2278
|
+
if (!refs.length) return void 0;
|
|
2279
|
+
const poly = plan.models.size > 1;
|
|
2280
|
+
const one = (r) => poly ? r : r.ref;
|
|
2281
|
+
return plan.multiple ? refs.map(one) : one(refs[0]);
|
|
2282
|
+
};
|
|
2283
|
+
const addressOf = (v) => {
|
|
2284
|
+
if (Array.isArray(v)) return v.map(addressOf).filter(Boolean);
|
|
2285
|
+
if (typeof v !== "number") return v;
|
|
2286
|
+
const t = postById.get(v);
|
|
2287
|
+
return t && t.status === "publish" && !t.password ? t.link ?? void 0 : void 0;
|
|
2288
|
+
};
|
|
1452
2289
|
const titleOf = (p) => {
|
|
1453
2290
|
const t = strip(p.title);
|
|
1454
2291
|
if (t) return t;
|
|
@@ -1788,31 +2625,32 @@ function rawToContentrain(raw, opts) {
|
|
|
1788
2625
|
label: "Comments open",
|
|
1789
2626
|
order: o += 10
|
|
1790
2627
|
};
|
|
1791
|
-
for (const p of typeItems) {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if (t && t !== "object") {
|
|
1796
|
-
fields[k] = {
|
|
1797
|
-
type: t,
|
|
1798
|
-
label: k,
|
|
1799
|
-
order: o += 10
|
|
1800
|
-
};
|
|
1801
|
-
report.meta_fields[k] = t;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
for (const [k, acf] of Object.entries(p.acf ?? {})) {
|
|
1805
|
-
if (fields[k]) continue;
|
|
1806
|
-
const t = byValue(acf.value) ?? "string";
|
|
2628
|
+
for (const p of typeItems) for (const [k, v] of Object.entries(p.meta)) {
|
|
2629
|
+
if (k.startsWith("_") || fields[k] || PLUGIN_META.test(k) || k === "enclosure") continue;
|
|
2630
|
+
const t = byValue(v);
|
|
2631
|
+
if (t && t !== "object") {
|
|
1807
2632
|
fields[k] = {
|
|
1808
|
-
type: t
|
|
2633
|
+
type: t,
|
|
1809
2634
|
label: k,
|
|
1810
|
-
description: "ACF",
|
|
1811
2635
|
order: o += 10
|
|
1812
2636
|
};
|
|
1813
|
-
report.
|
|
2637
|
+
report.meta_fields[k] = t;
|
|
1814
2638
|
}
|
|
1815
2639
|
}
|
|
2640
|
+
const acfFields = acfPlan(typeItems, (k) => !!fields[k]);
|
|
2641
|
+
for (const [k, plan] of acfFields) {
|
|
2642
|
+
const def = plan.def ?? {
|
|
2643
|
+
type: plan.multiple ? "relations" : "relation",
|
|
2644
|
+
model: plan.models.size > 1 ? [...plan.models].toSorted() : [...plan.models][0]
|
|
2645
|
+
};
|
|
2646
|
+
fields[k] = {
|
|
2647
|
+
...def,
|
|
2648
|
+
...def.label ? {} : { label: k },
|
|
2649
|
+
description: def.description ?? "ACF",
|
|
2650
|
+
order: o += 10
|
|
2651
|
+
};
|
|
2652
|
+
report.acf_fields[k] = plan.def ? plan.def.type : `${def.type}:${[...plan.models].toSorted().join("|")}`;
|
|
2653
|
+
}
|
|
1816
2654
|
const covered = locales.filter((l) => l === locale || typeItems.some((p) => postLocale(p) === l));
|
|
1817
2655
|
addModel({
|
|
1818
2656
|
id: mid,
|
|
@@ -1861,7 +2699,22 @@ function rawToContentrain(raw, opts) {
|
|
|
1861
2699
|
if (fields.visibility) e.visibility = p.password ? "password" : p.status === "private" ? "private" : "public";
|
|
1862
2700
|
if (fields.comments_open) e.comments_open = p.comment_status === "open";
|
|
1863
2701
|
for (const k of Object.keys(fields)) if (k in p.meta && !(k in e) && !k.startsWith("_")) e[k] = p.meta[k];
|
|
1864
|
-
for (const [k, acf] of Object.entries(p.acf ?? {}))
|
|
2702
|
+
for (const [k, acf] of Object.entries(p.acf ?? {})) {
|
|
2703
|
+
const plan = acfFields.get(k);
|
|
2704
|
+
if (!plan || k in e) continue;
|
|
2705
|
+
const ctx = {
|
|
2706
|
+
timeZone: raw.site.timezone,
|
|
2707
|
+
gmtOffset: raw.site.gmt_offset,
|
|
2708
|
+
dropped: () => {
|
|
2709
|
+
report.acf_outside_choices[k] = (report.acf_outside_choices[k] ?? 0) + 1;
|
|
2710
|
+
},
|
|
2711
|
+
unzoned: () => {
|
|
2712
|
+
report.acf_datetime_unzoned++;
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
const v = plan.def ? acfValue(plan.def, plan.kind === "address" ? addressOf(acf.value) : acfRows(acfScrub(acf.value)), ctx) : acfRefs(plan, acf.value);
|
|
2716
|
+
if (v !== void 0) e[k] = v;
|
|
2717
|
+
}
|
|
1865
2718
|
contentBucket[id] = e;
|
|
1866
2719
|
metaBucket[id] = mapStatus(p);
|
|
1867
2720
|
}
|
|
@@ -1993,7 +2846,7 @@ function rawToContentrain(raw, opts) {
|
|
|
1993
2846
|
name: strip(m.name),
|
|
1994
2847
|
slug: slugify(m.slug) || `menu-${m.id}`,
|
|
1995
2848
|
items: m.items.map((i) => itemRef(i.id)),
|
|
1996
|
-
wp_id: m.id,
|
|
2849
|
+
...m.id !== null && m.id > 0 ? { wp_id: m.id } : {},
|
|
1997
2850
|
...m.locations?.length ? { locations: m.locations } : {}
|
|
1998
2851
|
};
|
|
1999
2852
|
metas.menus[mid] = importMeta("published");
|
|
@@ -2157,7 +3010,7 @@ function rawToContentrain(raw, opts) {
|
|
|
2157
3010
|
}
|
|
2158
3011
|
});
|
|
2159
3012
|
siteEntry = pick({
|
|
2160
|
-
title: strip(raw.site.title)
|
|
3013
|
+
title: strip(raw.site.title),
|
|
2161
3014
|
tagline: raw.site.description,
|
|
2162
3015
|
url: raw.site.url,
|
|
2163
3016
|
language: raw.site.language
|
|
@@ -2168,6 +3021,7 @@ function rawToContentrain(raw, opts) {
|
|
|
2168
3021
|
"language"
|
|
2169
3022
|
]));
|
|
2170
3023
|
siteMeta = importMeta("published");
|
|
3024
|
+
report.site_title_missing = !siteEntry.title;
|
|
2171
3025
|
const vocabTerms = {};
|
|
2172
3026
|
for (const m of menus) for (const i of m.items) {
|
|
2173
3027
|
const k = slugify(strip(i.title));
|