@markdy/core 0.8.5 → 0.8.7

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.d.ts CHANGED
@@ -77,6 +77,12 @@ type Cue = {
77
77
  zoom?: number;
78
78
  dur?: number;
79
79
  line: number;
80
+ } | {
81
+ kind: "frame";
82
+ targets: string[];
83
+ zoom?: number;
84
+ dur?: number;
85
+ line: number;
80
86
  } | {
81
87
  kind: "use";
82
88
  pattern: string;
@@ -163,7 +169,7 @@ type RoutedEdge = {
163
169
  type TimedCue = {
164
170
  start: number;
165
171
  duration: number;
166
- kind: "show" | "hide" | "flow" | "glow" | "focus";
172
+ kind: "show" | "hide" | "flow" | "glow" | "focus" | "frame";
167
173
  targets: string[];
168
174
  edgeId?: string;
169
175
  segments?: FlowSegment[];
package/dist/index.js CHANGED
@@ -349,8 +349,9 @@ var EDGE_OPERATORS = {
349
349
  "~>": "event",
350
350
  "--": "dependency"
351
351
  };
352
- var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set(["show", "hide", "glow", "focus", "use"]);
353
- var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction"]);
352
+ var RESERVED_SELECTORS = /* @__PURE__ */ new Set(["$title", "$nodes", "$edges"]);
353
+ var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set(["show", "hide", "glow", "focus", "frame", "use"]);
354
+ var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction", "layout"]);
354
355
  function nodeRole(kind) {
355
356
  return TECHNICAL_NODE_KINDS[kind] ?? "compute";
356
357
  }
@@ -391,6 +392,7 @@ var DEFAULTS = {
391
392
  flow: 0.55,
392
393
  glow: 0.45,
393
394
  focus: 0.6,
395
+ frame: 0.7,
394
396
  beatGap: 0.14,
395
397
  cueGap: 0.08,
396
398
  stagger: 0.06
@@ -575,7 +577,7 @@ function scheduleBeats(ast, edges) {
575
577
  }
576
578
  return;
577
579
  }
578
- if (cue.kind !== "show" && cue.kind !== "hide" && cue.kind !== "glow" && cue.kind !== "focus") return;
580
+ if (cue.kind !== "show" && cue.kind !== "hide" && cue.kind !== "glow" && cue.kind !== "focus" && cue.kind !== "frame") return;
579
581
  const targets = resolveTargets(cue.targets, ast, Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members])));
580
582
  scheduled.push({
581
583
  start: t,
@@ -586,7 +588,7 @@ function scheduleBeats(ast, edges) {
586
588
  stagger: cue.kind === "show" ? cue.stagger ?? DEFAULTS.stagger : void 0,
587
589
  color: cue.kind === "glow" ? cue.color : void 0,
588
590
  strength: cue.kind === "glow" ? cue.strength : void 0,
589
- zoom: cue.kind === "focus" ? cue.zoom : void 0
591
+ zoom: cue.kind === "focus" || cue.kind === "frame" ? cue.zoom : void 0
590
592
  },
591
593
  beat: beat.name
592
594
  });
@@ -756,24 +758,79 @@ var ParseError = class extends Error {
756
758
  }
757
759
  };
758
760
  var FLOW_OP_RE = /(->|<-|~>|--)/;
759
- var PROP_RE = /(\w[\w.-]*)=(\S+)/g;
760
761
  function stripComment(line) {
761
- const idx = line.indexOf("//");
762
- return idx >= 0 ? line.slice(0, idx) : line;
762
+ let inString = false;
763
+ let escaped = false;
764
+ for (let i = 0; i < line.length; i++) {
765
+ const ch = line[i];
766
+ if (escaped) {
767
+ escaped = false;
768
+ continue;
769
+ }
770
+ if (inString && ch === "\\") {
771
+ escaped = true;
772
+ continue;
773
+ }
774
+ if (ch === '"') {
775
+ inString = !inString;
776
+ continue;
777
+ }
778
+ if (inString) continue;
779
+ if (ch === "/" && line[i + 1] === "/") return line.slice(0, i);
780
+ if (ch === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
781
+ }
782
+ return line;
783
+ }
784
+ function parsePropValue(raw) {
785
+ let val = raw;
786
+ if (raw.startsWith('"')) {
787
+ const parsed = parseStringToken(raw);
788
+ if (parsed && parsed.rest === "") val = parsed.value;
789
+ }
790
+ if (typeof val === "string") {
791
+ if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
792
+ else if (val === "true") val = true;
793
+ else if (val === "false") val = false;
794
+ else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
795
+ else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
796
+ }
797
+ return val;
763
798
  }
764
799
  function parseProps(raw) {
765
800
  const props = {};
766
- for (const match of raw.matchAll(PROP_RE)) {
767
- const key = match[1];
768
- let val = match[2];
769
- if (typeof val === "string") {
770
- if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
771
- else if (val === "true") val = true;
772
- else if (val === "false") val = false;
773
- else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
774
- else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
801
+ let i = 0;
802
+ while (i < raw.length) {
803
+ const ch = raw[i];
804
+ if (ch === '"') {
805
+ const parsed = parseStringToken(raw.slice(i));
806
+ if (!parsed) break;
807
+ i = raw.length - parsed.rest.length;
808
+ continue;
809
+ }
810
+ const keyMatch = raw.slice(i).match(/^(\w[\w.-]*)=/);
811
+ if (!keyMatch) {
812
+ i++;
813
+ continue;
775
814
  }
776
- props[key] = val;
815
+ const key = keyMatch[1];
816
+ i += key.length + 1;
817
+ let value = "";
818
+ if (raw[i] === '"') {
819
+ const start = i;
820
+ const parsed = parseStringToken(raw.slice(i));
821
+ if (!parsed) {
822
+ value = raw.slice(i);
823
+ i = raw.length;
824
+ } else {
825
+ i = raw.length - parsed.rest.length;
826
+ value = raw.slice(start, i).trim();
827
+ }
828
+ } else {
829
+ const start = i;
830
+ while (i < raw.length && !/\s/.test(raw[i])) i++;
831
+ value = raw.slice(start, i);
832
+ }
833
+ props[key] = parsePropValue(value);
777
834
  }
778
835
  return props;
779
836
  }
@@ -798,6 +855,56 @@ function parseStringToken(raw) {
798
855
  function splitTargets(raw) {
799
856
  return raw.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
800
857
  }
858
+ function splitOutsideQuotes(raw, separator) {
859
+ const parts = [];
860
+ let start = 0;
861
+ let inString = false;
862
+ let escaped = false;
863
+ for (let i = 0; i < raw.length; i++) {
864
+ const ch = raw[i];
865
+ if (escaped) {
866
+ escaped = false;
867
+ continue;
868
+ }
869
+ if (inString && ch === "\\") {
870
+ escaped = true;
871
+ continue;
872
+ }
873
+ if (ch === '"') {
874
+ inString = !inString;
875
+ continue;
876
+ }
877
+ if (!inString && ch === separator && /\s/.test(raw[i - 1] ?? "") && /\s/.test(raw[i + 1] ?? "")) {
878
+ parts.push(raw.slice(start, i).trim());
879
+ start = i + 1;
880
+ }
881
+ }
882
+ parts.push(raw.slice(start).trim());
883
+ return parts.filter(Boolean);
884
+ }
885
+ function stripCueProps(raw, keys) {
886
+ let inString = false;
887
+ let escaped = false;
888
+ for (let i = 0; i < raw.length; i++) {
889
+ const ch = raw[i];
890
+ if (escaped) {
891
+ escaped = false;
892
+ continue;
893
+ }
894
+ if (inString && ch === "\\") {
895
+ escaped = true;
896
+ continue;
897
+ }
898
+ if (ch === '"') {
899
+ inString = !inString;
900
+ continue;
901
+ }
902
+ if (inString || !/\s/.test(ch)) continue;
903
+ const rest = raw.slice(i + 1);
904
+ if (keys.some((key) => rest.startsWith(`${key}=`))) return raw.slice(0, i).trim();
905
+ }
906
+ return raw.trim();
907
+ }
801
908
  function parseFlowChain(line, lineNo) {
802
909
  const segments = [];
803
910
  const parts = line.split(FLOW_OP_RE).map((p) => p.trim()).filter(Boolean);
@@ -834,18 +941,17 @@ function splitTargetLabel(token, lineNo) {
834
941
  }
835
942
  function parseCueLine(line, lineNo) {
836
943
  const trimmed = line.trim();
837
- const propsMatch = trimmed.match(/\s+(?:dur|stagger|color|strength|zoom|after)=\S+/);
838
- const props = propsMatch ? parseProps(propsMatch[0]) : {};
839
- if (trimmed.includes(" & ")) {
840
- const parts = trimmed.split(/\s+&\s+/);
944
+ const props = parseProps(trimmed);
945
+ const parallelParts = splitOutsideQuotes(trimmed, "&");
946
+ if (parallelParts.length > 1) {
841
947
  return {
842
948
  kind: "parallel",
843
- cues: parts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
949
+ cues: parallelParts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
844
950
  line: lineNo
845
951
  };
846
952
  }
847
953
  if (FLOW_OP_RE.test(trimmed)) {
848
- const chainPart = trimmed.split(/\s+(?:dur|stagger|color|strength|zoom|after)=/)[0].trim();
954
+ const chainPart = stripCueProps(trimmed, ["dur", "stagger", "color", "strength", "zoom", "after"]);
849
955
  return {
850
956
  kind: "flow",
851
957
  segments: parseFlowChain(chainPart, lineNo),
@@ -856,7 +962,7 @@ function parseCueLine(line, lineNo) {
856
962
  const [head, ...rest] = trimmed.split(/\s+/);
857
963
  const keyword = head.toLowerCase();
858
964
  if (keyword === "show" || keyword === "hide") {
859
- const targetRaw = rest.join(" ").split(/\s+(?:dur|stagger)=/)[0];
965
+ const targetRaw = stripCueProps(rest.join(" "), ["dur", "stagger"]);
860
966
  return {
861
967
  kind: keyword,
862
968
  targets: splitTargets(targetRaw),
@@ -866,7 +972,7 @@ function parseCueLine(line, lineNo) {
866
972
  };
867
973
  }
868
974
  if (keyword === "glow") {
869
- const targetRaw = rest.join(" ").split(/\s+(?:color|strength|dur)=/)[0];
975
+ const targetRaw = stripCueProps(rest.join(" "), ["color", "strength", "dur"]);
870
976
  return {
871
977
  kind: "glow",
872
978
  targets: splitTargets(targetRaw),
@@ -877,7 +983,7 @@ function parseCueLine(line, lineNo) {
877
983
  };
878
984
  }
879
985
  if (keyword === "focus") {
880
- const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
986
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
881
987
  return {
882
988
  kind: "focus",
883
989
  targets: splitTargets(targetRaw),
@@ -886,6 +992,18 @@ function parseCueLine(line, lineNo) {
886
992
  line: lineNo
887
993
  };
888
994
  }
995
+ if (keyword === "frame") {
996
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
997
+ const targets = splitTargets(targetRaw);
998
+ if (targets.length === 0) throw new ParseError(`expected frame target`, lineNo);
999
+ return {
1000
+ kind: "frame",
1001
+ targets,
1002
+ zoom: typeof props.zoom === "number" ? props.zoom : void 0,
1003
+ dur: typeof props.dur === "number" ? props.dur : void 0,
1004
+ line: lineNo
1005
+ };
1006
+ }
889
1007
  if (keyword === "use") {
890
1008
  const call = rest.join(" ");
891
1009
  const m = call.match(/^(\w+)\s*\((.*)\)\s*$/);
@@ -931,10 +1049,9 @@ function substitutePatternCue(cue, params, args) {
931
1049
  params.forEach((p, i) => {
932
1050
  if (resolvedArgs[p] === void 0 && positional[i]) resolvedArgs[p] = positional[i];
933
1051
  });
934
- delete resolvedArgs.__pos_0;
935
- delete resolvedArgs.__pos_1;
936
- delete resolvedArgs.__pos_2;
937
- delete resolvedArgs.__pos_3;
1052
+ for (const key of Object.keys(resolvedArgs)) {
1053
+ if (key.startsWith("__pos_")) delete resolvedArgs[key];
1054
+ }
938
1055
  const sub = (s) => {
939
1056
  for (const p of params) {
940
1057
  if (resolvedArgs[p]) s = s.replaceAll(`$${p}`, resolvedArgs[p]);
@@ -952,7 +1069,7 @@ function substitutePatternCue(cue, params, args) {
952
1069
  }))
953
1070
  };
954
1071
  }
955
- if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus") {
1072
+ if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus" || cue.kind === "frame") {
956
1073
  return { ...cue, targets: cue.targets.map(sub) };
957
1074
  }
958
1075
  if (cue.kind === "parallel") {
@@ -980,6 +1097,91 @@ function readIndentedBody(blocks, startIdx, parentIndent) {
980
1097
  }
981
1098
  return { body, nextIdx: i };
982
1099
  }
1100
+ function readBody(blocks, startIdx, parentIndent, braceDelimited) {
1101
+ if (!braceDelimited) return readIndentedBody(blocks, startIdx, parentIndent);
1102
+ const body = [];
1103
+ let i = startIdx;
1104
+ while (i < blocks.length) {
1105
+ if (blocks[i].text === "}") return { body, nextIdx: i + 1 };
1106
+ body.push(blocks[i]);
1107
+ i++;
1108
+ }
1109
+ return { body, nextIdx: i };
1110
+ }
1111
+ function normalizeCueBlocks(blocks) {
1112
+ const normalized = [];
1113
+ for (const block of blocks) {
1114
+ if (block.text.startsWith("& ")) {
1115
+ const prev = normalized[normalized.length - 1];
1116
+ if (!prev) throw new ParseError(`parallel continuation must follow a cue`, block.line);
1117
+ prev.text = `${prev.text} ${block.text}`;
1118
+ continue;
1119
+ }
1120
+ normalized.push({ ...block });
1121
+ }
1122
+ return normalized;
1123
+ }
1124
+ function pushWarning(diagnostics, seen, line, message) {
1125
+ const key = `${line}:${message}`;
1126
+ if (seen.has(key)) return;
1127
+ seen.add(key);
1128
+ diagnostics.push({ severity: "warning", message, line });
1129
+ }
1130
+ function visitCues(cues, visit) {
1131
+ for (const cue of cues) {
1132
+ visit(cue);
1133
+ if (cue.kind === "parallel") visitCues(cue.cues, visit);
1134
+ }
1135
+ }
1136
+ function validateReferences(ast) {
1137
+ const seen = /* @__PURE__ */ new Set();
1138
+ const hasNode = (id) => Boolean(ast.nodes[id]);
1139
+ const hasGroup = (id) => Boolean(ast.groups[id]);
1140
+ const isKnownTarget = (target) => {
1141
+ if (RESERVED_SELECTORS.has(target)) return true;
1142
+ if (target.startsWith("$")) return hasGroup(target.slice(1));
1143
+ return hasNode(target) || hasGroup(target);
1144
+ };
1145
+ for (const group of Object.values(ast.groups)) {
1146
+ for (const member of group.members) {
1147
+ if (!hasNode(member)) {
1148
+ pushWarning(ast.diagnostics, seen, group.line, `group '${group.id}' references unknown node '${member}'`);
1149
+ }
1150
+ }
1151
+ }
1152
+ for (const node of Object.values(ast.nodes)) {
1153
+ if (node.style && !ast.styles[node.style]) {
1154
+ pushWarning(ast.diagnostics, seen, node.line, `node '${node.id}' references unknown style '${node.style}'`);
1155
+ }
1156
+ }
1157
+ const validateFlowEndpoint = (line, endpoint) => {
1158
+ if (!hasNode(endpoint)) {
1159
+ pushWarning(ast.diagnostics, seen, line, `flow references unknown node '${endpoint}'`);
1160
+ }
1161
+ };
1162
+ for (const edge of ast.edges) {
1163
+ validateFlowEndpoint(edge.line, edge.from);
1164
+ validateFlowEndpoint(edge.line, edge.to);
1165
+ }
1166
+ for (const beat of ast.beats) {
1167
+ visitCues(beat.cues, (cue) => {
1168
+ if (cue.kind === "flow") {
1169
+ for (const segment of cue.segments) {
1170
+ validateFlowEndpoint(cue.line, segment.from);
1171
+ validateFlowEndpoint(cue.line, segment.to);
1172
+ }
1173
+ return;
1174
+ }
1175
+ if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus" || cue.kind === "frame") {
1176
+ for (const target of cue.targets) {
1177
+ if (!isKnownTarget(target)) {
1178
+ pushWarning(ast.diagnostics, seen, cue.line, `${cue.kind} references unknown target '${target}'`);
1179
+ }
1180
+ }
1181
+ }
1182
+ });
1183
+ }
1184
+ }
983
1185
  function parse(source, opts = {}) {
984
1186
  const lines = source.replace(/\r\n/g, "\n").split("\n");
985
1187
  const diagnostics = [];
@@ -1012,6 +1214,11 @@ function parse(source, opts = {}) {
1012
1214
  title = str.value;
1013
1215
  remainder = str.rest;
1014
1216
  }
1217
+ const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
1218
+ if (inlineLayout) {
1219
+ meta.direction = inlineLayout[1].toUpperCase();
1220
+ remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
1221
+ }
1015
1222
  const props = parseProps(remainder);
1016
1223
  for (const [k, v] of Object.entries(props)) {
1017
1224
  if (!SCENE_KEYS.has(k)) {
@@ -1021,7 +1228,7 @@ function parse(source, opts = {}) {
1021
1228
  if (k === "width" || k === "height" || k === "fps") meta[k] = Number(v);
1022
1229
  else if (k === "duration") meta.duration = Number(v);
1023
1230
  else if (k === "theme") meta.theme = String(v);
1024
- else if (k === "direction") meta.direction = String(v).toUpperCase();
1231
+ else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
1025
1232
  }
1026
1233
  i++;
1027
1234
  continue;
@@ -1039,11 +1246,11 @@ function parse(source, opts = {}) {
1039
1246
  continue;
1040
1247
  }
1041
1248
  if (line.startsWith("pattern ")) {
1042
- const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*:\s*$/);
1249
+ const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*(?::|\{)\s*$/);
1043
1250
  if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
1044
1251
  const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
1045
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1046
- const cues = body.map((b) => parseCueLine(b.text, b.line));
1252
+ const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1253
+ const cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1047
1254
  patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
1048
1255
  i = nextIdx;
1049
1256
  continue;
@@ -1080,10 +1287,10 @@ function parse(source, opts = {}) {
1080
1287
  continue;
1081
1288
  }
1082
1289
  if (line.startsWith("beat ")) {
1083
- const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
1290
+ const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*(?::|\{)\s*$/);
1084
1291
  if (!m) throw new ParseError(`expected beat name:`, lineNo);
1085
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1086
- let cues = body.map((b) => parseCueLine(b.text, b.line));
1292
+ const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1293
+ let cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1087
1294
  cues = expandPatternCues(cues, patterns, lineNo);
1088
1295
  beats.push({ name: m[1], label: m[2], cues, line: lineNo });
1089
1296
  i = nextIdx;
@@ -1122,10 +1329,6 @@ function parse(source, opts = {}) {
1122
1329
  }
1123
1330
  throw new ParseError(`unexpected statement`, lineNo);
1124
1331
  }
1125
- const errors = diagnostics.filter((d) => d.severity === "error");
1126
- if (errors.length) {
1127
- throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
1128
- }
1129
1332
  const ast = {
1130
1333
  meta: { ...meta, title: title || void 0 },
1131
1334
  styles,
@@ -1136,6 +1339,11 @@ function parse(source, opts = {}) {
1136
1339
  beats,
1137
1340
  diagnostics
1138
1341
  };
1342
+ validateReferences(ast);
1343
+ const errors = ast.diagnostics.filter((d) => d.severity === "error");
1344
+ if (errors.length) {
1345
+ throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
1346
+ }
1139
1347
  if (!opts.parseOnly && Object.keys(nodes).length === 0 && beats.length === 0) {
1140
1348
  }
1141
1349
  return ast;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "MarkdyScript parser and diagram compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",