@open-feishu-card/adapter 0.2.1 → 0.3.1

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/dist/index.cjs CHANGED
@@ -642,6 +642,10 @@ function convertHeader(header) {
642
642
  const property = {};
643
643
  for (const [k, v] of Object.entries(header)) {
644
644
  if (k === "tag") continue;
645
+ if (k === "title" || k === "subtitle") {
646
+ property[k] = wrapHeaderText(v);
647
+ continue;
648
+ }
645
649
  if (TEXT_LEAF_KEYS.has(k)) {
646
650
  property[k] = wrapTextLeaf(v);
647
651
  continue;
@@ -806,6 +810,22 @@ function wrapTextLeaf(v) {
806
810
  }
807
811
  return { tag: "plain_text", property: { content: "" } };
808
812
  }
813
+ function wrapHeaderText(v) {
814
+ if (typeof v === "string") return { tag: "plain_text", property: { content: v } };
815
+ if (!isRecord(v)) return { tag: "plain_text", property: { content: "" } };
816
+ const tag = typeof v.tag === "string" ? v.tag : "plain_text";
817
+ const content = typeof v.content === "string" ? v.content : "";
818
+ if (tag === "lark_md" || tag === "markdown") {
819
+ return {
820
+ tag: "markdown",
821
+ property: {
822
+ elements: parseMarkdown(content),
823
+ originTag: "lark_md"
824
+ }
825
+ };
826
+ }
827
+ return wrapTextLeaf(v);
828
+ }
809
829
  function behaviorsToActions(behaviors) {
810
830
  const out = [];
811
831
  for (const b of behaviors) {
@@ -849,203 +869,1000 @@ function isRecord(v) {
849
869
  }
850
870
 
851
871
  // src/raw-to-dsl.ts
852
- var CAMEL_RENAME = {
853
- textAlign: "text_align",
854
- textStyle: "text_style",
855
- backgroundStyle: "background_style",
856
- backgroundColor: "background_color",
857
- userID: "user_id",
858
- showAvatar: "show_avatar",
859
- showName: "show_name",
860
- hasBorder: "has_border",
861
- hoverTips: "hover_tips",
862
- pillShaped: "pill_shaped",
863
- tagStyle: "tag_style",
864
- scaleType: "scale_type",
865
- dropInvalidUserID: "drop_invalid_user_id",
866
- udIcon: "ud_icon",
867
- // video / audio
868
- fileID: "file_key",
869
- coverImageID: "image_key",
870
- showTime: "show_time",
871
- enableDownload: "enable_download",
872
- audioID: "audio_id",
873
- showProgressBar: "show_progress_bar",
874
- fillColor: "fill_color"
875
- };
876
- var KNOWN_TAGS = /* @__PURE__ */ new Set([
877
- "markdown",
878
- "lark_md",
879
- "div",
880
- "column_set",
881
- "column",
882
- "hr",
883
- "image",
884
- "img",
885
- "button",
886
- "overflow",
887
- "select_static",
888
- "select_multi",
889
- "input",
890
- "date_picker",
891
- "picker_time",
892
- "picker_date",
893
- "checker",
894
- "chart",
895
- "table",
896
- "iframe",
897
- "person",
898
- "person_list",
899
- "interactive_container",
900
- "tag",
901
- "text_tag",
902
- "multi_image",
903
- "plain_text",
904
- "lark_md_inline",
905
- "image_text",
906
- "carousel",
907
- "grid",
908
- "speaking",
909
- "vote",
910
- "lottery",
911
- "calender",
912
- "schedule",
913
- "share_chat",
914
- "share_user",
915
- "add_on",
916
- "whole_text",
917
- "cropper_image",
918
- "highlight",
919
- "local_file",
920
- "file_preview",
921
- "input_select",
922
- "picker_datetime",
923
- "picker_multi",
924
- "rule",
925
- "statistic",
926
- "task",
927
- "card_header",
928
- "body",
929
- "audio",
930
- "video",
931
- "markdown_audio",
932
- "unknown"
933
- ]);
934
- function isRecord2(v) {
935
- return typeof v === "object" && v !== null && !Array.isArray(v);
936
- }
937
- function degradeUnknown(el) {
938
- const rawTag = typeof el.tag === "string" ? el.tag : "undefined";
939
- return { tag: "unknown", raw_tag: rawTag, fallback: el };
872
+ function isRawCardEnvelope(value) {
873
+ if (typeof value !== "object" || value === null) return false;
874
+ const v = value;
875
+ return typeof v.json_card === "string" && typeof v.card_schema === "number";
940
876
  }
941
- function rawToDsl(raw) {
942
- let card = {};
943
- if (isRecord2(raw)) {
944
- const env = isRecord2(raw.json_card) ? raw.json_card : isRecord2(raw.card) ? raw.card : raw;
945
- if (isRecord2(env)) card = env;
877
+ function rawCardToDsl(envelope) {
878
+ if (!isRawCardEnvelope(envelope)) return envelope;
879
+ const env = envelope;
880
+ let inner;
881
+ try {
882
+ inner = JSON.parse(env.json_card ?? "");
883
+ } catch {
884
+ return envelope;
946
885
  }
947
- const out = {
948
- schema: typeof card.schema === "string" ? card.schema : "2.0"
886
+ const ctx = {
887
+ images: env.json_attachment?.images ?? {},
888
+ atUsers: env.json_attachment?.at_users ?? {}
949
889
  };
950
- if (isRecord2(card.config)) out.config = card.config;
951
- if (isRecord2(card.header)) out.header = convertHeaderToDsl(card.header);
952
- if (isRecord2(card.body)) out.body = convertBodyToDsl(card.body);
953
- else if (Array.isArray(card.elements)) {
954
- out.body = { elements: card.elements.map(convertElToDsl).filter(Boolean) };
890
+ return convertRoot(inner, ctx);
891
+ }
892
+ var RAW_ONLY_FIELDS = /* @__PURE__ */ new Set([
893
+ "new_body",
894
+ "source",
895
+ // Editor line-count mirror on markdown/plain_text. Schema 2.0 has no `lines`
896
+ // field; Feishu rejects it with code 200621 ("unknown property, property:
897
+ // lines"). Raw emits it on every text node (lines:0), so drop it everywhere.
898
+ "lines"
899
+ ]);
900
+ function isI18nMirror(key) {
901
+ return key.startsWith("i18n");
902
+ }
903
+ var IMG_RAW_ONLY_FIELDS = /* @__PURE__ */ new Set([
904
+ "compact_width",
905
+ "custom_width",
906
+ "mode"
907
+ ]);
908
+ var VIDEO_RAW_ONLY_FIELDS = /* @__PURE__ */ new Set([
909
+ // duration lives on im.file.create upload, not on the video component.
910
+ "duration",
911
+ "duration_ms",
912
+ // editor instance id — public audio has audio_id, video does NOT have video_id.
913
+ "video_id"
914
+ ]);
915
+ var VIDEO_PUBLIC_FIELDS = /* @__PURE__ */ new Set([
916
+ "element_id",
917
+ "margin",
918
+ "file_key",
919
+ "i18n_file_key",
920
+ "enable_download",
921
+ "show_time",
922
+ "cover",
923
+ "hover_tips",
924
+ "fallback"
925
+ ]);
926
+ var AUDIO_PUBLIC_FIELDS = /* @__PURE__ */ new Set([
927
+ "element_id",
928
+ "margin",
929
+ "file_key",
930
+ "i18n_file_key",
931
+ "audio_id",
932
+ "disabled",
933
+ "disabled_tips",
934
+ "show_progress_bar",
935
+ "show_time",
936
+ "time_display",
937
+ "time_position",
938
+ "border",
939
+ "style",
940
+ "padding",
941
+ "background_style",
942
+ "fill_color",
943
+ "width",
944
+ "fallback"
945
+ ]);
946
+ var TAG_DEFAULT_DROPS = {
947
+ column: {
948
+ disabled: false
949
+ },
950
+ column_set: {
951
+ disabled: false,
952
+ horizontal_spacing: "default"
953
+ },
954
+ button: {
955
+ disabled: false
956
+ },
957
+ interactive_container: {
958
+ disabled: false,
959
+ required: false,
960
+ has_border: false,
961
+ height: "auto"
955
962
  }
963
+ };
964
+ var VALUE_ECHO_TARGETS = {
965
+ horizontal_spacing_value: "horizontal_spacing",
966
+ vertical_spacing_value: "vertical_spacing",
967
+ width_value: "width",
968
+ height_value: "height",
969
+ corner_radius_value: "corner_radius",
970
+ size_value: "size"
971
+ };
972
+ function convertRoot(root, ctx) {
973
+ if (!isRecord2(root)) return root;
974
+ const out = {};
975
+ out.schema = typeof root.schema === "string" ? root.schema : "2.0";
976
+ out.config = convertConfig(isRecord2(root.config) ? root.config : {});
977
+ if (isRecord2(root.body)) out.body = convertBody2(root.body, ctx);
978
+ if (isRecord2(root.header)) out.header = convertHeader2(root.header, ctx);
956
979
  return out;
957
980
  }
958
- function convertHeaderToDsl(header) {
959
- const property = isRecord2(header.property) ? header.property : header;
960
- const out = {};
961
- for (const [k, v] of Object.entries(property)) {
962
- if (k === "ud_icon" || k === "udIcon") {
963
- if (isRecord2(v) && typeof v.tag === "string") {
964
- out.ud_icon = convertElToDsl(v);
965
- continue;
981
+ function convertConfig(_node) {
982
+ return { update_multi: true };
983
+ }
984
+ function convertHeader2(node, ctx) {
985
+ const property = isRecord2(node.property) ? node.property : node;
986
+ const out = convertProperty(property, "header", ctx);
987
+ delete out.tag;
988
+ return out;
989
+ }
990
+ function convertHeaderText(rawValue, ctx) {
991
+ const tag = typeof rawValue.tag === "string" ? rawValue.tag : "";
992
+ const wrapped = isRecord2(rawValue.property);
993
+ const property = wrapped ? rawValue.property : rawValue;
994
+ if (tag === "plain_text") {
995
+ const content = typeof property.content === "string" ? property.content : "";
996
+ return { tag: "plain_text", content };
997
+ }
998
+ if (tag === "markdown" || tag === "lark_md") {
999
+ if (wrapped && Array.isArray(property.elements)) {
1000
+ const rebuilt = rebuildMarkdown(property, ctx);
1001
+ return { tag: "lark_md", content: typeof rebuilt.content === "string" ? rebuilt.content : "" };
1002
+ }
1003
+ const content = typeof property.content === "string" ? property.content : "";
1004
+ return { tag: "lark_md", content };
1005
+ }
1006
+ if (!wrapped && typeof rawValue.content === "string" && !tag) {
1007
+ return { tag: "plain_text", content: rawValue.content };
1008
+ }
1009
+ return void 0;
1010
+ }
1011
+ function convertBody2(node, ctx) {
1012
+ const property = isRecord2(node.property) ? node.property : node;
1013
+ return convertProperty(property, "body", ctx);
1014
+ }
1015
+ function unwrap(node, ctx) {
1016
+ const tag = typeof node.tag === "string" ? node.tag : void 0;
1017
+ const property = isRecord2(node.property) ? node.property : {};
1018
+ if (tag === "markdown") return rebuildMarkdown(property, ctx);
1019
+ if (tag === "plain_text") {
1020
+ const content = typeof property.content === "string" ? property.content : "";
1021
+ return { tag: "plain_text", content };
1022
+ }
1023
+ if (tag === "standard_icon") {
1024
+ const out = { tag: "standard_icon" };
1025
+ if (typeof property.token === "string") out.token = property.token;
1026
+ if (typeof property.color === "string") out.color = property.color;
1027
+ const widthPx = isPixelScalar(property.widthValue) ? pixelOf(property.widthValue) : void 0;
1028
+ const heightPx = isPixelScalar(property.heightValue) ? pixelOf(property.heightValue) : void 0;
1029
+ if (widthPx !== void 0 && heightPx !== void 0) {
1030
+ out.size = `${widthPx}px ${heightPx}px`;
1031
+ } else if (isRecord2(property.size) && hasSide(property.size)) out.size = formatBox(property.size);
1032
+ else if (isPixelScalar(property.size)) out.size = `${pixelOf(property.size)}px`;
1033
+ else if (typeof property.size === "string") out.size = property.size;
1034
+ return out;
1035
+ }
1036
+ if (tag === "ud_icon") {
1037
+ const token = typeof property.token === "string" ? property.token : void 0;
1038
+ const out = { tag: "standard_icon" };
1039
+ if (token) out.token = token;
1040
+ return out;
1041
+ }
1042
+ if (tag === "chart") {
1043
+ const out = { tag: "chart" };
1044
+ if (typeof property.colorTheme === "string") out.color_theme = property.colorTheme;
1045
+ if (typeof property.preview === "boolean") out.preview = property.preview;
1046
+ if (property.chartSpec !== void 0) out.chart_spec = property.chartSpec;
1047
+ return out;
1048
+ }
1049
+ if (tag === "table") {
1050
+ const out = { tag: "table" };
1051
+ const rawColumns = Array.isArray(property.columns) ? property.columns : [];
1052
+ out.columns = rawColumns.map((col) => {
1053
+ if (!isRecord2(col)) return {};
1054
+ const c = {};
1055
+ if (typeof col.name === "string") c.name = col.name;
1056
+ const displayName = typeof col.displayName === "string" ? col.displayName : typeof col.display_name === "string" ? col.display_name : void 0;
1057
+ if (displayName !== void 0) c.display_name = displayName;
1058
+ const dataType = typeof col.dataType === "string" ? col.dataType : typeof col.data_type === "string" ? col.data_type : void 0;
1059
+ if (dataType !== void 0) c.data_type = dataType;
1060
+ const hAlign = typeof col.horizontalAlign === "string" ? col.horizontalAlign : typeof col.horizontal_align === "string" ? col.horizontal_align : void 0;
1061
+ if (hAlign !== void 0) c.horizontal_align = hAlign;
1062
+ const wv = col.widthValue;
1063
+ if (isRecord2(wv)) {
1064
+ if (wv.type === "pixels" && typeof wv.value === "number") c.width = `${wv.value}px`;
1065
+ else if (typeof wv.value === "string") c.width = wv.value;
1066
+ } else if (typeof col.width === "string" || typeof col.width === "number") {
1067
+ c.width = col.width;
1068
+ }
1069
+ if (isRecord2(col.format)) c.format = col.format;
1070
+ return c;
1071
+ });
1072
+ const rawRows = Array.isArray(property.rows) ? property.rows : [];
1073
+ out.rows = rawRows.map((row) => {
1074
+ if (!isRecord2(row)) return {};
1075
+ const r = {};
1076
+ for (const [k, cell] of Object.entries(row)) {
1077
+ let value = isRecord2(cell) && "data" in cell ? cell.data : cell;
1078
+ if (isRecord2(value) && value.tag === "markdown" && isRecord2(value.property)) {
1079
+ const elements = Array.isArray(value.property.elements) ? value.property.elements : [];
1080
+ value = renderChildren(elements, ctx);
1081
+ }
1082
+ r[k] = value;
966
1083
  }
1084
+ return r;
1085
+ });
1086
+ if (isRecord2(property.headerStyle)) {
1087
+ const hs = {};
1088
+ for (const [k, v] of Object.entries(property.headerStyle)) {
1089
+ const sk = snake(k);
1090
+ if (sk === "platform_text_size") continue;
1091
+ if (sk === "lines_value") {
1092
+ if (isRecord2(v) && typeof v.value === "number") hs.lines = v.value;
1093
+ continue;
1094
+ }
1095
+ hs[sk] = v;
1096
+ }
1097
+ out.header_style = hs;
967
1098
  }
968
- out[CAMEL_RENAME[k] ?? k] = unwrapTextLeaf(v);
1099
+ const rhv = property.rowHeightValue;
1100
+ if (isRecord2(rhv) && typeof rhv.value === "string") out.row_height = rhv.value;
1101
+ else if (typeof property.rowHeight === "string") out.row_height = property.rowHeight;
1102
+ if (typeof property.pageSize === "number") out.page_size = property.pageSize;
1103
+ if (typeof property.freezeFirstColumn === "boolean") out.freeze_first_column = property.freezeFirstColumn;
1104
+ return out;
969
1105
  }
970
- return out;
1106
+ if (tag === "input") {
1107
+ const allow = /* @__PURE__ */ new Set([
1108
+ "name",
1109
+ "placeholder",
1110
+ "default_value",
1111
+ "value",
1112
+ "label",
1113
+ "label_position",
1114
+ "margin",
1115
+ "max_length",
1116
+ "input_type",
1117
+ "width"
1118
+ ]);
1119
+ const out = { tag: "input" };
1120
+ const converted2 = convertProperty(property, "input", ctx);
1121
+ for (const [k, v] of Object.entries(converted2)) {
1122
+ if (allow.has(k)) out[k] = v;
1123
+ }
1124
+ if (converted2.required === true) out.required = true;
1125
+ return out;
1126
+ }
1127
+ if (tag === "person" || tag === "person_v1") {
1128
+ const out = { tag: "person" };
1129
+ if (typeof property.userID === "string") out.user_id = property.userID;
1130
+ else if (typeof property.user_id === "string") out.user_id = property.user_id;
1131
+ if (typeof property.size === "string") out.size = property.size;
1132
+ if (typeof property.showAvatar === "boolean") out.show_avatar = property.showAvatar;
1133
+ if (typeof property.showName === "boolean") out.show_name = property.showName;
1134
+ if (typeof property.style === "string") out.style = property.style;
1135
+ if (isRecord2(property.margin) && hasSide(property.margin)) out.margin = formatBox(property.margin);
1136
+ else if (typeof property.margin === "string") out.margin = property.margin;
1137
+ return out;
1138
+ }
1139
+ if (tag === "person_list") {
1140
+ const out = { tag: "person_list" };
1141
+ if (typeof property.size === "string") out.size = property.size;
1142
+ if (typeof property.showAvatar === "boolean") out.show_avatar = property.showAvatar;
1143
+ if (typeof property.showName === "boolean") out.show_name = property.showName;
1144
+ if (typeof property.lines === "number") out.lines = property.lines;
1145
+ if (typeof property.dropInvalidUserID === "boolean") out.drop_invalid_user_id = property.dropInvalidUserID;
1146
+ else if (typeof property.drop_invalid_user_id === "boolean") out.drop_invalid_user_id = property.drop_invalid_user_id;
1147
+ if (isRecord2(property.margin) && hasSide(property.margin)) out.margin = formatBox(property.margin);
1148
+ else if (typeof property.margin === "string") out.margin = property.margin;
1149
+ const rawPersons = Array.isArray(property.persons) ? property.persons : [];
1150
+ out.persons = rawPersons.map((p) => {
1151
+ if (!isRecord2(p)) return void 0;
1152
+ const id = typeof p.id === "string" ? p.id : typeof p.userID === "string" ? p.userID : void 0;
1153
+ return id ? { id } : void 0;
1154
+ }).filter((p) => p !== void 0);
1155
+ const iconSrc = isRecord2(property.icon) ? property.icon : isRecord2(property.udIcon) ? property.udIcon : void 0;
1156
+ if (iconSrc) {
1157
+ const ip = isRecord2(iconSrc.property) ? iconSrc.property : iconSrc;
1158
+ const icon = {};
1159
+ const iconTag = typeof iconSrc.tag === "string" ? iconSrc.tag : typeof ip.tag === "string" ? ip.tag : "standard_icon";
1160
+ icon.tag = iconTag === "ud_icon" ? "standard_icon" : iconTag;
1161
+ if (typeof ip.token === "string") icon.token = ip.token;
1162
+ if (typeof ip.color === "string") icon.color = ip.color;
1163
+ if (typeof ip.imageID === "string") icon.img_key = ip.imageID;
1164
+ else if (typeof ip.img_key === "string") icon.img_key = ip.img_key;
1165
+ if (icon.token !== void 0 || icon.img_key !== void 0) out.icon = icon;
1166
+ }
1167
+ return out;
1168
+ }
1169
+ if (tag === "div" && isRecord2(property.text) && property.fields === void 0) {
1170
+ const text = property.text;
1171
+ const textTag = typeof text.tag === "string" ? text.tag : "";
1172
+ if ((textTag === "markdown" || textTag === "lark_md") && isRecord2(text.property)) {
1173
+ return unwrap(text, ctx);
1174
+ }
1175
+ if (textTag === "plain_text") {
1176
+ const out = { tag: "div" };
1177
+ const widthVal = property.widthValue;
1178
+ if (isRecord2(widthVal) && typeof widthVal.value === "string" && typeof widthVal.type === "string" && widthVal.type.startsWith("builtin_")) {
1179
+ out.width = widthVal.value;
1180
+ } else if (typeof property.width === "string") {
1181
+ out.width = property.width;
1182
+ }
1183
+ const tp = isRecord2(text.property) ? text.property : {};
1184
+ const content = typeof tp.content === "string" ? tp.content : "";
1185
+ const textAlign = typeof tp.textAlign === "string" ? tp.textAlign : "left";
1186
+ out.text = { tag: "plain_text", content, text_align: textAlign };
1187
+ if (isRecord2(property.icon)) {
1188
+ out.icon = unwrap(property.icon, ctx);
1189
+ }
1190
+ return out;
1191
+ }
1192
+ }
1193
+ const converted = convertProperty(property, tag, ctx);
1194
+ return tag ? { tag, ...converted } : converted;
971
1195
  }
972
- function convertBodyToDsl(body) {
973
- const property = isRecord2(body.property) ? body.property : body;
1196
+ var DROP = /* @__PURE__ */ Symbol("drop");
1197
+ function convertProperty(property, ownerTag, ctx) {
974
1198
  const out = {};
975
- for (const [k, v] of Object.entries(property)) {
976
- if (k === "elements" && Array.isArray(v)) {
977
- out.elements = v.map(convertElToDsl).filter(Boolean);
1199
+ const siblings = {
1200
+ buttonActionType: ownerTag === "button" && typeof property.actionType === "string" ? property.actionType : void 0
1201
+ };
1202
+ for (const [rawKey, rawValue] of Object.entries(property)) {
1203
+ const result = convertField(rawKey, rawValue, ownerTag, ctx, siblings);
1204
+ if (result === DROP) continue;
1205
+ const [outKey, outValue] = result;
1206
+ if (outValue === DROP) continue;
1207
+ out[outKey] = outValue;
1208
+ }
1209
+ for (const [echoKey, enumKey] of Object.entries(VALUE_ECHO_TARGETS)) {
1210
+ if (!(echoKey in out)) continue;
1211
+ const echoVal = out[echoKey];
1212
+ const enumVal = out[enumKey];
1213
+ delete out[echoKey];
1214
+ if (!isRecord2(echoVal)) continue;
1215
+ const echoType = typeof echoVal.type === "string" ? echoVal.type : "";
1216
+ const echoValue = echoVal.value;
1217
+ if (echoType.includes("pixels") && typeof echoValue === "number") {
1218
+ if (enumVal === void 0 || enumVal === "default" || enumVal === "auto" || enumVal === "weighted") {
1219
+ out[enumKey] = `${echoValue}px`;
1220
+ }
1221
+ continue;
1222
+ }
1223
+ if (echoType === "aspect_ratio" && isRecord2(echoValue)) {
1224
+ const w = echoValue.width;
1225
+ const h = echoValue.height;
1226
+ if (typeof w === "number" && typeof h === "number") {
1227
+ out["size"] = `${w}:${h}`;
1228
+ }
978
1229
  continue;
979
1230
  }
980
- if (isRecord2(v) && typeof v.tag === "string") {
981
- out[CAMEL_RENAME[k] ?? k] = convertElToDsl(v);
1231
+ if (echoType === "width_height_pixels" && isRecord2(echoValue)) {
1232
+ const w = echoValue.width;
1233
+ const h = echoValue.height;
1234
+ if (typeof w === "number" && typeof h === "number") {
1235
+ out["size"] = `${w}px ${h}px`;
1236
+ }
982
1237
  continue;
983
1238
  }
984
- if (Array.isArray(v)) {
985
- out[CAMEL_RENAME[k] ?? k] = v.map((item) => isRecord2(item) && typeof item.tag === "string" ? convertElToDsl(item) : item);
1239
+ if (echoType === "builtin_image_size") continue;
1240
+ if (echoType.startsWith("builtin_") && typeof echoValue === "string") {
1241
+ if (enumVal === void 0) out[enumKey] = echoValue;
986
1242
  continue;
987
1243
  }
988
- out[CAMEL_RENAME[k] ?? k] = unwrapBox(v);
1244
+ }
1245
+ if (ownerTag && TAG_DEFAULT_DROPS[ownerTag]) {
1246
+ const defaults = TAG_DEFAULT_DROPS[ownerTag];
1247
+ for (const [k, defVal] of Object.entries(defaults)) {
1248
+ if (out[k] === defVal) delete out[k];
1249
+ }
1250
+ }
1251
+ if (ownerTag === "video") {
1252
+ for (const k of Object.keys(out)) {
1253
+ if (!VIDEO_PUBLIC_FIELDS.has(k)) delete out[k];
1254
+ }
1255
+ }
1256
+ if (ownerTag === "audio") {
1257
+ for (const k of Object.keys(out)) {
1258
+ if (!AUDIO_PUBLIC_FIELDS.has(k)) delete out[k];
1259
+ }
989
1260
  }
990
1261
  return out;
991
1262
  }
992
- function convertElToDsl(el) {
993
- if (!isRecord2(el)) return void 0;
994
- const tag = typeof el.tag === "string" ? el.tag : "";
995
- if (!tag) return void 0;
996
- if (!KNOWN_TAGS.has(tag)) return degradeUnknown(el);
997
- if ("fallback" in el && isRecord2(el.fallback)) return el;
998
- const property = isRecord2(el.property) ? el.property : {};
999
- const out = {};
1000
- if (tag !== "plain_text") out.tag = tag === "img" ? "image" : tag;
1001
- for (const [k, v] of Object.entries(property)) {
1002
- const key = CAMEL_RENAME[k] ?? k;
1003
- if (k === "elements" && Array.isArray(v)) {
1004
- if (tag === "markdown") {
1005
- out.content = elementsToMarkdown(v);
1006
- continue;
1263
+ function convertField(rawKey, rawValue, ownerTag, ctx, siblings = {}) {
1264
+ if (rawKey === "originTag" || rawKey === "markdownElements") return DROP;
1265
+ const key = snake(rawKey);
1266
+ if (RAW_ONLY_FIELDS.has(key)) return DROP;
1267
+ if (isI18nMirror(key)) return DROP;
1268
+ if (key === "actions" && Array.isArray(rawValue) && rawValue.length === 0) {
1269
+ return DROP;
1270
+ }
1271
+ if ((key === "padding" || key === "margin") && isRecord2(rawValue) && hasSide(rawValue)) {
1272
+ return [key, formatBox(rawValue)];
1273
+ }
1274
+ if (key in VALUE_ECHO_TARGETS) {
1275
+ return [key, rawValue];
1276
+ }
1277
+ if (isPixelScalar(rawValue)) {
1278
+ return [key, `${pixelOf(rawValue)}px`];
1279
+ }
1280
+ if (ownerTag === "img" && key === "image_id") {
1281
+ const id = typeof rawValue === "string" || typeof rawValue === "number" ? String(rawValue) : void 0;
1282
+ const origin = id !== void 0 ? ctx.images[id]?.origin_key : void 0;
1283
+ if (origin) return ["img_key", origin];
1284
+ return [key, rawValue];
1285
+ }
1286
+ if (ownerTag === "img" && IMG_RAW_ONLY_FIELDS.has(key)) return DROP;
1287
+ if (ownerTag === "video" && key === "cover_image_id") {
1288
+ const imgKey = resolveAttachmentImgKey(rawValue, ctx);
1289
+ if (!imgKey) return DROP;
1290
+ return ["cover", { img_key: imgKey }];
1291
+ }
1292
+ if (ownerTag === "video" && key === "cover") {
1293
+ const cover = normalizeVideoCover(rawValue, ctx);
1294
+ return cover ? ["cover", cover] : DROP;
1295
+ }
1296
+ if ((ownerTag === "video" || ownerTag === "audio") && key === "file_id") {
1297
+ if (typeof rawValue === "string" && rawValue) return ["file_key", rawValue];
1298
+ return DROP;
1299
+ }
1300
+ if (ownerTag === "video" && VIDEO_RAW_ONLY_FIELDS.has(key)) return DROP;
1301
+ if ((key === "alt" || key === "hover_tips") && isRecord2(rawValue) && rawValue.tag === "plain_text") {
1302
+ const p = isRecord2(rawValue.property) ? rawValue.property : {};
1303
+ return [key, { tag: "plain_text", content: typeof p.content === "string" ? p.content : "" }];
1304
+ }
1305
+ if (key === "text" && isRecord2(rawValue) && rawValue.tag === "plain_text") {
1306
+ const p = isRecord2(rawValue.property) ? rawValue.property : {};
1307
+ return [key, { tag: "plain_text", content: typeof p.content === "string" ? p.content : "" }];
1308
+ }
1309
+ if (ownerTag === "header" && (key === "title" || key === "subtitle") && isRecord2(rawValue)) {
1310
+ const headerText = convertHeaderText(rawValue, ctx);
1311
+ if (headerText) return [key, headerText];
1312
+ }
1313
+ if (ownerTag === "header" && key === "ud_icon" && isRecord2(rawValue) && rawValue.tag === "ud_icon") {
1314
+ const p = isRecord2(rawValue.property) ? rawValue.property : {};
1315
+ const token = typeof p.token === "string" ? p.token : void 0;
1316
+ if (!token) return DROP;
1317
+ return ["icon", { tag: "standard_icon", token }];
1318
+ }
1319
+ if (ownerTag === "collapsible_panel" && key === "header" && isRecord2(rawValue)) {
1320
+ const cleaned = {};
1321
+ for (const [k, v] of Object.entries(rawValue)) {
1322
+ const sk = snake(k);
1323
+ if (sk === "position" || sk === "width" || sk === "vertical_align") continue;
1324
+ const sub = convertField(k, v, "collapsible_panel_header", ctx, siblings);
1325
+ if (sub === DROP) continue;
1326
+ const [subKey, subVal] = sub;
1327
+ if (subVal === DROP) continue;
1328
+ cleaned[subKey] = subVal;
1329
+ }
1330
+ return [key, cleaned];
1331
+ }
1332
+ if (ownerTag === "button" && key === "action_type") return DROP;
1333
+ if (ownerTag === "button" && key === "actions" && Array.isArray(rawValue)) {
1334
+ const openUrls = [];
1335
+ for (const a of rawValue) {
1336
+ if (!isRecord2(a)) continue;
1337
+ const inner = isRecord2(a.action) ? a.action : {};
1338
+ const t = typeof a.type === "string" ? a.type : void 0;
1339
+ if (t === "open_url" && typeof inner.url === "string") openUrls.push(inner.url);
1340
+ }
1341
+ if (openUrls.length === 0) return DROP;
1342
+ if (siblings.buttonActionType === "link") {
1343
+ return ["multi_url", { url: openUrls[0] }];
1344
+ }
1345
+ return [
1346
+ "behaviors",
1347
+ openUrls.map((url) => ({ type: "open_url", default_url: url }))
1348
+ ];
1349
+ }
1350
+ if (ownerTag === "interactive_container" && key === "actions" && Array.isArray(rawValue)) {
1351
+ const behaviors = rawValue.map((a) => {
1352
+ if (!isRecord2(a)) return void 0;
1353
+ const inner = isRecord2(a.action) ? a.action : {};
1354
+ const t = typeof a.type === "string" ? a.type : void 0;
1355
+ if (t === "open_url" && typeof inner.url === "string") {
1356
+ return { type: "open_url", default_url: inner.url };
1007
1357
  }
1008
- out.elements = v.map(convertElToDsl).filter(Boolean);
1009
- continue;
1358
+ if (t === "callback") {
1359
+ return { type: "callback", value: inner.value ?? {} };
1360
+ }
1361
+ return void 0;
1362
+ }).filter((b) => b !== void 0);
1363
+ if (behaviors.length === 0) return DROP;
1364
+ return ["behaviors", behaviors];
1365
+ }
1366
+ if (key === "border" && isRecord2(rawValue)) {
1367
+ const b = rawValue;
1368
+ const out = {};
1369
+ if (typeof b.borderColor === "string") out.color = b.borderColor;
1370
+ else if (typeof b.color === "string") out.color = b.color;
1371
+ if (isPixelScalar(b.cornerRadius)) out.corner_radius = `${pixelOf(b.cornerRadius)}px`;
1372
+ else if (typeof b.cornerRadius === "string") out.corner_radius = b.cornerRadius;
1373
+ else if (b.cornerRadius !== void 0) out.corner_radius = b.cornerRadius;
1374
+ for (const [bk, bv] of Object.entries(b)) {
1375
+ if (bk === "borderColor" || bk === "cornerRadius" || bk === "color") continue;
1376
+ out[snake(bk)] = convertGeneric(bv, ctx);
1010
1377
  }
1011
- if (k === "columns" && Array.isArray(v)) {
1012
- out.columns = v.map(convertElToDsl).filter(Boolean);
1378
+ return [key, out];
1379
+ }
1380
+ if (key === "url" && isRecord2(rawValue)) {
1381
+ const v = rawValue;
1382
+ const main = typeof v.url === "string" ? v.url : "";
1383
+ const others = ["androidURL", "iosURL", "pcURL", "harmonyURL"].map((k) => v[k]).filter((x) => typeof x === "string" && x.length > 0);
1384
+ if (others.length === 0) return [key, main];
1385
+ return [
1386
+ key,
1387
+ {
1388
+ url: main,
1389
+ android_url: typeof v.androidURL === "string" ? v.androidURL : "",
1390
+ ios_url: typeof v.iosURL === "string" ? v.iosURL : "",
1391
+ pc_url: typeof v.pcURL === "string" ? v.pcURL : ""
1392
+ }
1393
+ ];
1394
+ }
1395
+ return [key, convertGeneric(rawValue, ctx)];
1396
+ }
1397
+ function renderInlinePerson(prop) {
1398
+ const id = typeof prop.userID === "string" ? prop.userID : typeof prop.user_id === "string" ? prop.user_id : typeof prop.id === "string" ? prop.id : "";
1399
+ if (!id) return "";
1400
+ const parts = [`id='${id}'`];
1401
+ if (typeof prop.showName === "boolean") parts.push(`show_name=${prop.showName}`);
1402
+ else if (typeof prop.show_name === "boolean") parts.push(`show_name=${prop.show_name}`);
1403
+ if (typeof prop.showAvatar === "boolean") parts.push(`show_avatar=${prop.showAvatar}`);
1404
+ else if (typeof prop.show_avatar === "boolean") parts.push(`show_avatar=${prop.show_avatar}`);
1405
+ if (typeof prop.style === "string") parts.push(`style='${prop.style}'`);
1406
+ return `<person ${parts.join(" ")}></person>`;
1407
+ }
1408
+ function renderInlineAudio(prop) {
1409
+ const fileKey = typeof prop.fileID === "string" ? prop.fileID : typeof prop.file_key === "string" ? prop.file_key : typeof prop.file_id === "string" ? prop.file_id : "";
1410
+ if (!fileKey) return "";
1411
+ const attrs = [`file_key='${fileKey}'`];
1412
+ const audioId = typeof prop.audioID === "string" ? prop.audioID : typeof prop.audio_id === "string" ? prop.audio_id : void 0;
1413
+ if (audioId) attrs.push(`audio_id='${audioId}'`);
1414
+ if (prop.showTime === true || prop.show_time === true) attrs.push("show_time=true");
1415
+ const style = typeof prop.style === "string" ? prop.style : void 0;
1416
+ if (style) attrs.push(`style='${style}'`);
1417
+ const bg = typeof prop.backgroundColor === "string" ? prop.backgroundColor : typeof prop.background_color === "string" ? prop.background_color : void 0;
1418
+ if (bg) attrs.push(`background_color='${bg}'`);
1419
+ const fill = typeof prop.fillColor === "string" ? prop.fillColor : typeof prop.fill_color === "string" ? prop.fill_color : void 0;
1420
+ if (fill) attrs.push(`fill_color='${fill}'`);
1421
+ const urlObj = isRecord2(prop.url) ? prop.url : void 0;
1422
+ if (urlObj) {
1423
+ if (typeof urlObj.url === "string" && urlObj.url) attrs.push(`fallback_url='${urlObj.url}'`);
1424
+ if (typeof urlObj.pcURL === "string" && urlObj.pcURL) attrs.push(`fallback_pc_url='${urlObj.pcURL}'`);
1425
+ else if (typeof urlObj.pc_url === "string" && urlObj.pc_url) attrs.push(`fallback_pc_url='${urlObj.pc_url}'`);
1426
+ if (typeof urlObj.iosURL === "string" && urlObj.iosURL) attrs.push(`fallback_ios_url='${urlObj.iosURL}'`);
1427
+ else if (typeof urlObj.ios_url === "string" && urlObj.ios_url) attrs.push(`fallback_ios_url='${urlObj.ios_url}'`);
1428
+ if (typeof urlObj.androidURL === "string" && urlObj.androidURL) attrs.push(`fallback_android_url='${urlObj.androidURL}'`);
1429
+ else if (typeof urlObj.android_url === "string" && urlObj.android_url) attrs.push(`fallback_android_url='${urlObj.android_url}'`);
1430
+ if (typeof urlObj.harmonyURL === "string" && urlObj.harmonyURL) attrs.push(`fallback_harmony_url='${urlObj.harmonyURL}'`);
1431
+ else if (typeof urlObj.harmony_url === "string" && urlObj.harmony_url) attrs.push(`fallback_harmony_url='${urlObj.harmony_url}'`);
1432
+ } else if (typeof prop.fallback_url === "string" && prop.fallback_url) {
1433
+ attrs.push(`fallback_url='${prop.fallback_url}'`);
1434
+ }
1435
+ const fb = isRecord2(prop.fallbackText) ? prop.fallbackText : isRecord2(prop.fallback_text) ? prop.fallback_text : void 0;
1436
+ if (fb) {
1437
+ const fbProp = isRecord2(fb.property) ? fb.property : fb;
1438
+ const fbContent = typeof fbProp.content === "string" ? fbProp.content : typeof fb.content === "string" ? fb.content : "";
1439
+ if (fbContent) attrs.push(`fallback_text='${fbContent}'`);
1440
+ }
1441
+ return `<audio ${attrs.join(" ")}></audio>`;
1442
+ }
1443
+ function resolveAtUserId(prop, ctx) {
1444
+ if (typeof prop.userID === "string" && prop.userID) return prop.userID;
1445
+ if (typeof prop.userId === "string" && prop.userId) return prop.userId;
1446
+ if (typeof prop.user_id === "string" && prop.user_id) return prop.user_id;
1447
+ const key = typeof prop.atUserKey === "string" ? prop.atUserKey : typeof prop.at_user_key === "string" ? prop.at_user_key : void 0;
1448
+ if (!key || !ctx.atUsers[key]) return void 0;
1449
+ if (/^(ou_|on_)/.test(key)) return key;
1450
+ return ctx.atUsers[key].user_id;
1451
+ }
1452
+ function rebuildMarkdown(property, ctx) {
1453
+ const out = { tag: "markdown" };
1454
+ const elements = Array.isArray(property.elements) ? property.elements : [];
1455
+ const content = renderChildren(elements, ctx);
1456
+ if (content) out.content = content;
1457
+ for (const [rawKey, rawValue] of Object.entries(property)) {
1458
+ if (rawKey === "elements" || rawKey === "markdownElements" || rawKey === "originTag") continue;
1459
+ if (rawKey === "udIcon") continue;
1460
+ if (rawKey === "textStyle") {
1461
+ const ts = isRecord2(rawValue) ? rawValue : {};
1462
+ const size = typeof ts.size === "string" ? ts.size : void 0;
1463
+ if (size) out.text_size = size;
1013
1464
  continue;
1014
1465
  }
1015
- if (isRecord2(v) && typeof v.tag === "string") {
1016
- out[key] = convertElToDsl(v);
1017
- continue;
1466
+ const result = convertField(rawKey, rawValue, "markdown", ctx);
1467
+ if (result === DROP) continue;
1468
+ const [outKey, outValue] = result;
1469
+ if (outValue === DROP) continue;
1470
+ out[outKey] = outValue;
1471
+ }
1472
+ return out;
1473
+ }
1474
+ function wrapEmphasis(text, bold, color) {
1475
+ if (!bold && !color) return text;
1476
+ const lead = text.match(/^\s*/)[0];
1477
+ const trail = text.match(/\s*$/)[0];
1478
+ const core = text.slice(lead.length, text.length - trail.length);
1479
+ if (core.length === 0) return text;
1480
+ let wrapped = core;
1481
+ if (bold) wrapped = `**${wrapped}**`;
1482
+ if (color) wrapped = `<font color='${color}'>${wrapped}</font>`;
1483
+ return lead + wrapped + trail;
1484
+ }
1485
+ function renderChildren(children, ctx) {
1486
+ let out = "";
1487
+ let buffer = [];
1488
+ function flushBuffer() {
1489
+ if (buffer.length === 0) return;
1490
+ let group = [];
1491
+ function emitGroup() {
1492
+ if (group.length === 0) return;
1493
+ const inner = group.map((s) => s.content).join("");
1494
+ const { bold, color } = group[0];
1495
+ out += wrapEmphasis(inner, bold, color);
1496
+ group = [];
1018
1497
  }
1019
- if (Array.isArray(v)) {
1020
- out[key] = v.map((item) => isRecord2(item) && typeof item.tag === "string" ? convertElToDsl(item) : item);
1498
+ for (const seg of buffer) {
1499
+ if (group.length === 0) {
1500
+ group.push(seg);
1501
+ continue;
1502
+ }
1503
+ const head = group[0];
1504
+ if (head.bold === seg.bold && head.color === seg.color) group.push(seg);
1505
+ else {
1506
+ emitGroup();
1507
+ group.push(seg);
1508
+ }
1509
+ }
1510
+ emitGroup();
1511
+ buffer = [];
1512
+ }
1513
+ for (const child of children) {
1514
+ if (!isRecord2(child)) continue;
1515
+ const inline = childToInlineSegment(child, ctx);
1516
+ if (inline) {
1517
+ buffer.push(inline);
1021
1518
  continue;
1022
1519
  }
1023
- out[key] = unwrapBox(unwrapTextLeaf(v));
1520
+ flushBuffer();
1521
+ out += childToMarkdown(child, ctx);
1024
1522
  }
1523
+ flushBuffer();
1025
1524
  return out;
1026
1525
  }
1027
- function unwrapTextLeaf(v) {
1028
- if (isRecord2(v) && v.tag === "plain_text") {
1029
- const p = isRecord2(v.property) ? v.property : {};
1030
- return typeof p.content === "string" ? p.content : "";
1526
+ function childToInlineSegment(child, ctx) {
1527
+ const tag = child.tag;
1528
+ const prop = isRecord2(child.property) ? child.property : {};
1529
+ const ts = isRecord2(prop.textStyle) ? prop.textStyle : void 0;
1530
+ const bold = !!(ts && Array.isArray(ts.attributes) && ts.attributes.includes("bold"));
1531
+ const rawColor = ts && typeof ts.color === "string" ? ts.color : void 0;
1532
+ const color = rawColor === "default" ? void 0 : rawColor;
1533
+ switch (tag) {
1534
+ case "plain_text":
1535
+ return {
1536
+ content: typeof prop.content === "string" ? prop.content : "",
1537
+ bold,
1538
+ color
1539
+ };
1540
+ case "code_span":
1541
+ return {
1542
+ content: `\`${typeof prop.content === "string" ? prop.content : ""}\``,
1543
+ bold,
1544
+ color
1545
+ };
1546
+ case "link": {
1547
+ const text = typeof prop.content === "string" ? prop.content : "";
1548
+ const urlObj = isRecord2(prop.url) ? prop.url : void 0;
1549
+ const url = urlObj && typeof urlObj.url === "string" ? urlObj.url : "";
1550
+ return { content: `[${text}](${url})`, bold, color };
1551
+ }
1552
+ case "at_all":
1553
+ return { content: "<at id=all></at>", bold, color };
1554
+ case "at": {
1555
+ const userId = resolveAtUserId(prop, ctx);
1556
+ return { content: userId ? `<at id=${userId}></at>` : "<at></at>", bold, color };
1557
+ }
1558
+ case "text_tag": {
1559
+ const tagColor = typeof prop.color === "string" ? prop.color : void 0;
1560
+ const inner = isRecord2(prop.text) ? prop.text : void 0;
1561
+ const innerProp = inner && isRecord2(inner.property) ? inner.property : void 0;
1562
+ const innerContent = innerProp && typeof innerProp.content === "string" ? innerProp.content : "";
1563
+ const rendered = tagColor ? `<text_tag color='${tagColor}'>${innerContent}</text_tag>` : `<text_tag>${innerContent}</text_tag>`;
1564
+ return { content: rendered, bold: false };
1565
+ }
1566
+ case "emoji": {
1567
+ const key = typeof prop.key === "string" ? prop.key : typeof prop.emojiKey === "string" ? prop.emojiKey : "";
1568
+ return key ? { content: `:${key}:`, bold: false } : void 0;
1569
+ }
1570
+ case "person":
1571
+ case "person_v1": {
1572
+ const rendered = renderInlinePerson(prop);
1573
+ return rendered ? { content: rendered, bold: false } : void 0;
1574
+ }
1575
+ case "markdown_audio": {
1576
+ const rendered = renderInlineAudio(prop);
1577
+ return rendered ? { content: rendered, bold: false } : void 0;
1578
+ }
1579
+ default:
1580
+ return void 0;
1031
1581
  }
1032
- return v;
1033
1582
  }
1034
- function unwrapBox(v) {
1035
- if (!isRecord2(v) || !isRecord2(v.top) || !isRecord2(v.right)) return v;
1036
- const side = (s) => isRecord2(s) && typeof s.value === "number" ? `${s.value}px` : "0px";
1037
- return `${side(v.top)} ${side(v.right)} ${side(v.bottom ?? v.top)} ${side(v.left ?? v.right)}`;
1583
+ function childToMarkdown(child, ctx) {
1584
+ const tag = child.tag;
1585
+ const prop = isRecord2(child.property) ? child.property : {};
1586
+ switch (tag) {
1587
+ case "plain_text": {
1588
+ const content = typeof prop.content === "string" ? prop.content : "";
1589
+ const ts = isRecord2(prop.textStyle) ? prop.textStyle : void 0;
1590
+ const color = ts && typeof ts.color === "string" ? ts.color : void 0;
1591
+ const attrs = ts && Array.isArray(ts.attributes) ? ts.attributes : [];
1592
+ return wrapEmphasis(content, attrs.includes("bold"), color);
1593
+ }
1594
+ case "br":
1595
+ return "\n";
1596
+ case "emoji": {
1597
+ const key = typeof prop.key === "string" ? prop.key : typeof prop.emojiKey === "string" ? prop.emojiKey : "";
1598
+ return key ? `:${key}:` : "";
1599
+ }
1600
+ case "at_all":
1601
+ return "<at id=all></at>";
1602
+ case "at": {
1603
+ const userId = resolveAtUserId(prop, ctx);
1604
+ return userId ? `<at id=${userId}></at>` : "<at></at>";
1605
+ }
1606
+ case "person":
1607
+ case "person_v1":
1608
+ return renderInlinePerson(prop);
1609
+ case "markdown_audio":
1610
+ return renderInlineAudio(prop);
1611
+ case "link": {
1612
+ const text = typeof prop.content === "string" ? prop.content : "";
1613
+ const urlObj = isRecord2(prop.url) ? prop.url : void 0;
1614
+ const url = urlObj && typeof urlObj.url === "string" ? urlObj.url : "";
1615
+ return `[${text}](${url})`;
1616
+ }
1617
+ case "code_span": {
1618
+ const text = typeof prop.content === "string" ? prop.content : "";
1619
+ return `\`${text}\``;
1620
+ }
1621
+ case "text_tag": {
1622
+ const color = typeof prop.color === "string" ? prop.color : void 0;
1623
+ const inner = isRecord2(prop.text) ? prop.text : void 0;
1624
+ const innerProp = inner && isRecord2(inner.property) ? inner.property : void 0;
1625
+ const innerContent = innerProp && typeof innerProp.content === "string" ? innerProp.content : "";
1626
+ if (color) return `<text_tag color='${color}'>${innerContent}</text_tag>`;
1627
+ return `<text_tag>${innerContent}</text_tag>`;
1628
+ }
1629
+ case "code_block": {
1630
+ const lang = typeof prop.language === "string" ? prop.language : "";
1631
+ const blocks = Array.isArray(prop.contents) ? prop.contents : [];
1632
+ const lines = blocks.map((blk) => {
1633
+ if (!isRecord2(blk)) return "";
1634
+ const inner = Array.isArray(blk.contents) ? blk.contents : [];
1635
+ let line = "";
1636
+ for (const seg of inner) {
1637
+ if (isRecord2(seg) && typeof seg.content === "string") line += seg.content;
1638
+ }
1639
+ return line;
1640
+ });
1641
+ const body = lines.join("\n").replace(/\n+$/, "");
1642
+ return `
1643
+ \`\`\`${lang}
1644
+ ${body}
1645
+ \`\`\`
1646
+ `;
1647
+ }
1648
+ case "list": {
1649
+ const items = Array.isArray(prop.items) ? prop.items : [];
1650
+ const lines = [];
1651
+ const levelCounters = [];
1652
+ for (const item of items) {
1653
+ if (!isRecord2(item)) continue;
1654
+ const level = typeof item.level === "number" ? item.level : 0;
1655
+ const type = typeof item.type === "string" ? item.type : "ul";
1656
+ const indent = " ".repeat(level);
1657
+ const inner = Array.isArray(item.elements) ? item.elements : [];
1658
+ let text = "";
1659
+ for (const seg of inner) {
1660
+ if (!isRecord2(seg)) continue;
1661
+ text += childToMarkdown(seg, ctx);
1662
+ }
1663
+ if (type === "ol") {
1664
+ let n;
1665
+ if (typeof item.order === "number" && Number.isFinite(item.order) && item.order > 0) {
1666
+ n = Math.floor(item.order);
1667
+ levelCounters.length = level + 1;
1668
+ levelCounters[level] = n;
1669
+ } else {
1670
+ levelCounters.length = level + 1;
1671
+ levelCounters[level] = (levelCounters[level] ?? 0) + 1;
1672
+ n = levelCounters[level];
1673
+ }
1674
+ lines.push(`${indent}${n}. ${text}`);
1675
+ } else {
1676
+ lines.push(`${indent}- ${text}`);
1677
+ }
1678
+ }
1679
+ return `
1680
+ ${lines.join("\n")}
1681
+
1682
+ `;
1683
+ }
1684
+ case "heading": {
1685
+ const level = typeof prop.level === "number" ? prop.level : 1;
1686
+ const inner = Array.isArray(prop.elements) ? prop.elements : [];
1687
+ let body = "";
1688
+ for (const seg of inner) {
1689
+ if (isRecord2(seg)) body += childToMarkdown(seg, ctx);
1690
+ }
1691
+ return `
1692
+
1693
+ ${"#".repeat(level)} ${body}
1694
+
1695
+ `;
1696
+ }
1697
+ case "hr": {
1698
+ return "\n\n---\n\n";
1699
+ }
1700
+ case "table": {
1701
+ const columns = Array.isArray(prop.columns) ? prop.columns : [];
1702
+ const rows = Array.isArray(prop.rows) ? prop.rows : [];
1703
+ if (columns.length === 0) return "";
1704
+ const headers = columns.map(
1705
+ (col) => isRecord2(col) && typeof col.displayName === "string" ? col.displayName : ""
1706
+ );
1707
+ const headerLine = `| ${headers.join(" | ")} |`;
1708
+ const separator = `| ${columns.map(() => "--------").join(" | ")} |`;
1709
+ const bodyLines = rows.map((row) => {
1710
+ if (!isRecord2(row)) return `| ${columns.map(() => "").join(" | ")} |`;
1711
+ const cells = columns.map((col) => {
1712
+ const name = isRecord2(col) && typeof col.name === "string" ? col.name : "";
1713
+ const cell = isRecord2(row[name]) ? row[name] : void 0;
1714
+ const data = cell && isRecord2(cell.data) ? cell.data : void 0;
1715
+ if (!data || data.tag !== "markdown") return "";
1716
+ const cellProp = isRecord2(data.property) ? data.property : {};
1717
+ const inner = Array.isArray(cellProp.elements) ? cellProp.elements : [];
1718
+ const text = renderChildren(inner, ctx);
1719
+ return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
1720
+ });
1721
+ return `| ${cells.join(" | ")} |`;
1722
+ });
1723
+ return `
1724
+
1725
+ ${headerLine}
1726
+ ${separator}
1727
+ ${bodyLines.join("\n")}
1728
+
1729
+ `;
1730
+ }
1731
+ case "blockquote": {
1732
+ const inner = Array.isArray(prop.elements) ? prop.elements : [];
1733
+ let body = "";
1734
+ for (const seg of inner) {
1735
+ if (isRecord2(seg)) body += childToMarkdown(seg, ctx);
1736
+ }
1737
+ return `
1738
+
1739
+ > ${body.replace(/\n/g, "\n> ")}
1740
+
1741
+ `;
1742
+ }
1743
+ default:
1744
+ return typeof prop.content === "string" ? prop.content : "";
1745
+ }
1746
+ }
1747
+ function convertGeneric(value, ctx) {
1748
+ if (Array.isArray(value)) {
1749
+ return value.map((item) => {
1750
+ if (isRecord2(item) && typeof item.tag === "string" && isRecord2(item.property)) {
1751
+ return unwrap(item, ctx);
1752
+ }
1753
+ return convertGeneric(item, ctx);
1754
+ });
1755
+ }
1756
+ if (isRecord2(value)) {
1757
+ if (typeof value.tag === "string" && isRecord2(value.property)) {
1758
+ return unwrap(value, ctx);
1759
+ }
1760
+ if (hasSide(value)) {
1761
+ return formatBox(value);
1762
+ }
1763
+ if (isPixelScalar(value)) {
1764
+ return `${pixelOf(value)}px`;
1765
+ }
1766
+ const out = {};
1767
+ for (const [k, v] of Object.entries(value)) {
1768
+ out[snake(k)] = convertGeneric(v, ctx);
1769
+ }
1770
+ return out;
1771
+ }
1772
+ return value;
1773
+ }
1774
+ function isRecord2(v) {
1775
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1038
1776
  }
1039
- function elementsToMarkdown(els) {
1040
- const parts = [];
1041
- for (const el of els) {
1042
- const p = isRecord2(el?.property) ? el.property : {};
1043
- if (typeof p.content === "string") parts.push(p.content);
1044
- else if (Array.isArray(p.elements)) parts.push(elementsToMarkdown(p.elements));
1045
- }
1046
- return parts.join("");
1777
+ function resolveAttachmentImgKey(rawValue, ctx) {
1778
+ if (typeof rawValue !== "string" && typeof rawValue !== "number") return void 0;
1779
+ const id = String(rawValue);
1780
+ const origin = ctx.images[id]?.origin_key;
1781
+ if (typeof origin === "string" && origin) return origin;
1782
+ if (id.startsWith("img_")) return id;
1783
+ return void 0;
1784
+ }
1785
+ function normalizeVideoCover(rawValue, ctx) {
1786
+ if (typeof rawValue === "string" || typeof rawValue === "number") {
1787
+ const imgKey2 = resolveAttachmentImgKey(rawValue, ctx);
1788
+ return imgKey2 ? { img_key: imgKey2 } : void 0;
1789
+ }
1790
+ if (!isRecord2(rawValue)) return void 0;
1791
+ if (typeof rawValue.img_key === "string" && rawValue.img_key) {
1792
+ return { img_key: rawValue.img_key };
1793
+ }
1794
+ const candidate = rawValue.imageID ?? rawValue.image_id ?? rawValue.cover_image_id ?? rawValue.coverImageId;
1795
+ const imgKey = resolveAttachmentImgKey(candidate, ctx);
1796
+ return imgKey ? { img_key: imgKey } : void 0;
1797
+ }
1798
+ function hasSide(v) {
1799
+ return ("top" in v || "bottom" in v || "left" in v || "right" in v) && Object.values(v).some((side) => isRecord2(side) && "type" in side && "value" in side);
1800
+ }
1801
+ function formatBox(v) {
1802
+ return `${pixelOf(v.top)}px ${pixelOf(v.right)}px ${pixelOf(v.bottom)}px ${pixelOf(v.left)}px`;
1803
+ }
1804
+ function isPixelScalar(v) {
1805
+ if (!isRecord2(v)) return false;
1806
+ return typeof v.type === "string" && v.type.includes("pixels") && typeof v.value === "number";
1807
+ }
1808
+ function pixelOf(side) {
1809
+ if (!isRecord2(side)) return 0;
1810
+ const v = side.value;
1811
+ if (typeof v === "number") return v;
1812
+ if (typeof v === "string") {
1813
+ const n = Number(v);
1814
+ return Number.isFinite(n) ? n : 0;
1815
+ }
1816
+ return 0;
1817
+ }
1818
+ function snake(key) {
1819
+ return key.replace(/ID$/g, "_id").replace(/URL$/g, "_url").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
1820
+ }
1821
+ function rawToDsl(raw) {
1822
+ if (isRawCardEnvelope(raw)) return rawCardToDsl(raw);
1823
+ if (typeof raw === "string") {
1824
+ try {
1825
+ return rawToDsl(JSON.parse(raw));
1826
+ } catch {
1827
+ return raw;
1828
+ }
1829
+ }
1830
+ if (!isRecord2(raw)) return raw;
1831
+ if (typeof raw.json_card === "string" || isRecord2(raw.json_card)) {
1832
+ let inner = raw.json_card;
1833
+ if (typeof inner === "string") {
1834
+ try {
1835
+ inner = JSON.parse(inner);
1836
+ } catch {
1837
+ return raw;
1838
+ }
1839
+ }
1840
+ const attachment = isRecord2(raw.json_attachment) ? raw.json_attachment : {};
1841
+ const ctx = {
1842
+ images: isRecord2(attachment.images) ? attachment.images : {},
1843
+ atUsers: isRecord2(attachment.at_users) ? attachment.at_users : {}
1844
+ };
1845
+ return convertRoot(inner, ctx);
1846
+ }
1847
+ if (isRecord2(raw.card)) {
1848
+ const card = raw.card;
1849
+ if (typeof card.json_card === "string" || isRecord2(card.json_card)) {
1850
+ return rawToDsl(card);
1851
+ }
1852
+ return convertRoot(card, { images: {}, atUsers: {} });
1853
+ }
1854
+ if ("body" in raw || "header" in raw || "schema" in raw) {
1855
+ return convertRoot(raw, { images: {}, atUsers: {} });
1856
+ }
1857
+ return raw;
1858
+ }
1859
+ function degradeUnknown(el) {
1860
+ const rawTag = typeof el.tag === "string" ? el.tag : "undefined";
1861
+ return { tag: "unknown", raw_tag: rawTag, fallback: el };
1047
1862
  }
1048
1863
 
1049
1864
  exports.degradeUnknown = degradeUnknown;
1050
1865
  exports.dslToRaw = dslToRaw;
1866
+ exports.isRawCardEnvelope = isRawCardEnvelope;
1867
+ exports.rawCardToDsl = rawCardToDsl;
1051
1868
  exports.rawToDsl = rawToDsl;