@markdy/core 0.8.6 → 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.
Files changed (2) hide show
  1. package/dist/index.js +163 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -351,7 +351,7 @@ var EDGE_OPERATORS = {
351
351
  };
352
352
  var RESERVED_SELECTORS = /* @__PURE__ */ new Set(["$title", "$nodes", "$edges"]);
353
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"]);
354
+ var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction", "layout"]);
355
355
  function nodeRole(kind) {
356
356
  return TECHNICAL_NODE_KINDS[kind] ?? "compute";
357
357
  }
@@ -758,24 +758,79 @@ var ParseError = class extends Error {
758
758
  }
759
759
  };
760
760
  var FLOW_OP_RE = /(->|<-|~>|--)/;
761
- var PROP_RE = /(\w[\w.-]*)=(\S+)/g;
762
761
  function stripComment(line) {
763
- const idx = line.indexOf("//");
764
- 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;
765
798
  }
766
799
  function parseProps(raw) {
767
800
  const props = {};
768
- for (const match of raw.matchAll(PROP_RE)) {
769
- const key = match[1];
770
- let val = match[2];
771
- if (typeof val === "string") {
772
- if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
773
- else if (val === "true") val = true;
774
- else if (val === "false") val = false;
775
- else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
776
- 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;
777
809
  }
778
- props[key] = val;
810
+ const keyMatch = raw.slice(i).match(/^(\w[\w.-]*)=/);
811
+ if (!keyMatch) {
812
+ i++;
813
+ continue;
814
+ }
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);
779
834
  }
780
835
  return props;
781
836
  }
@@ -800,6 +855,56 @@ function parseStringToken(raw) {
800
855
  function splitTargets(raw) {
801
856
  return raw.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
802
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
+ }
803
908
  function parseFlowChain(line, lineNo) {
804
909
  const segments = [];
805
910
  const parts = line.split(FLOW_OP_RE).map((p) => p.trim()).filter(Boolean);
@@ -837,16 +942,16 @@ function splitTargetLabel(token, lineNo) {
837
942
  function parseCueLine(line, lineNo) {
838
943
  const trimmed = line.trim();
839
944
  const props = parseProps(trimmed);
840
- if (trimmed.includes(" & ")) {
841
- const parts = trimmed.split(/\s+&\s+/);
945
+ const parallelParts = splitOutsideQuotes(trimmed, "&");
946
+ if (parallelParts.length > 1) {
842
947
  return {
843
948
  kind: "parallel",
844
- cues: parts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
949
+ cues: parallelParts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
845
950
  line: lineNo
846
951
  };
847
952
  }
848
953
  if (FLOW_OP_RE.test(trimmed)) {
849
- const chainPart = trimmed.split(/\s+(?:dur|stagger|color|strength|zoom|after)=/)[0].trim();
954
+ const chainPart = stripCueProps(trimmed, ["dur", "stagger", "color", "strength", "zoom", "after"]);
850
955
  return {
851
956
  kind: "flow",
852
957
  segments: parseFlowChain(chainPart, lineNo),
@@ -857,7 +962,7 @@ function parseCueLine(line, lineNo) {
857
962
  const [head, ...rest] = trimmed.split(/\s+/);
858
963
  const keyword = head.toLowerCase();
859
964
  if (keyword === "show" || keyword === "hide") {
860
- const targetRaw = rest.join(" ").split(/\s+(?:dur|stagger)=/)[0];
965
+ const targetRaw = stripCueProps(rest.join(" "), ["dur", "stagger"]);
861
966
  return {
862
967
  kind: keyword,
863
968
  targets: splitTargets(targetRaw),
@@ -867,7 +972,7 @@ function parseCueLine(line, lineNo) {
867
972
  };
868
973
  }
869
974
  if (keyword === "glow") {
870
- const targetRaw = rest.join(" ").split(/\s+(?:color|strength|dur)=/)[0];
975
+ const targetRaw = stripCueProps(rest.join(" "), ["color", "strength", "dur"]);
871
976
  return {
872
977
  kind: "glow",
873
978
  targets: splitTargets(targetRaw),
@@ -878,7 +983,7 @@ function parseCueLine(line, lineNo) {
878
983
  };
879
984
  }
880
985
  if (keyword === "focus") {
881
- const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
986
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
882
987
  return {
883
988
  kind: "focus",
884
989
  targets: splitTargets(targetRaw),
@@ -888,7 +993,7 @@ function parseCueLine(line, lineNo) {
888
993
  };
889
994
  }
890
995
  if (keyword === "frame") {
891
- const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
996
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
892
997
  const targets = splitTargets(targetRaw);
893
998
  if (targets.length === 0) throw new ParseError(`expected frame target`, lineNo);
894
999
  return {
@@ -992,6 +1097,30 @@ function readIndentedBody(blocks, startIdx, parentIndent) {
992
1097
  }
993
1098
  return { body, nextIdx: i };
994
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
+ }
995
1124
  function pushWarning(diagnostics, seen, line, message) {
996
1125
  const key = `${line}:${message}`;
997
1126
  if (seen.has(key)) return;
@@ -1085,6 +1214,11 @@ function parse(source, opts = {}) {
1085
1214
  title = str.value;
1086
1215
  remainder = str.rest;
1087
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
+ }
1088
1222
  const props = parseProps(remainder);
1089
1223
  for (const [k, v] of Object.entries(props)) {
1090
1224
  if (!SCENE_KEYS.has(k)) {
@@ -1094,7 +1228,7 @@ function parse(source, opts = {}) {
1094
1228
  if (k === "width" || k === "height" || k === "fps") meta[k] = Number(v);
1095
1229
  else if (k === "duration") meta.duration = Number(v);
1096
1230
  else if (k === "theme") meta.theme = String(v);
1097
- else if (k === "direction") meta.direction = String(v).toUpperCase();
1231
+ else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
1098
1232
  }
1099
1233
  i++;
1100
1234
  continue;
@@ -1112,11 +1246,11 @@ function parse(source, opts = {}) {
1112
1246
  continue;
1113
1247
  }
1114
1248
  if (line.startsWith("pattern ")) {
1115
- const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*:\s*$/);
1249
+ const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*(?::|\{)\s*$/);
1116
1250
  if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
1117
1251
  const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
1118
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1119
- 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));
1120
1254
  patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
1121
1255
  i = nextIdx;
1122
1256
  continue;
@@ -1153,10 +1287,10 @@ function parse(source, opts = {}) {
1153
1287
  continue;
1154
1288
  }
1155
1289
  if (line.startsWith("beat ")) {
1156
- const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
1290
+ const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*(?::|\{)\s*$/);
1157
1291
  if (!m) throw new ParseError(`expected beat name:`, lineNo);
1158
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1159
- 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));
1160
1294
  cues = expandPatternCues(cues, patterns, lineNo);
1161
1295
  beats.push({ name: m[1], label: m[2], cues, line: lineNo });
1162
1296
  i = nextIdx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "0.8.6",
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",