marc-ts 0.1.0 → 0.2.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/dist/marcjson.js CHANGED
@@ -1,39 +1,38 @@
1
- import { n as b, t as w } from "./types-c4Mo9m9u.js";
2
- function p(e) {
3
- const i = typeof e == "string" ? JSON.parse(e) : e;
4
- if (typeof i.leader != "string") throw new Error('MARC-in-JSON: missing or non-string "leader"');
5
- if (!Array.isArray(i.fields)) throw new Error('MARC-in-JSON: missing or non-array "fields"');
6
- const r = [];
7
- for (const t of i.fields) {
8
- if (typeof t != "object" || t === null || Array.isArray(t)) throw new Error("MARC-in-JSON: each field entry must be an object");
9
- const o = Object.keys(t);
1
+ import { n as p, t as w } from "./types-c4Mo9m9u.js";
2
+ function f(r) {
3
+ if (typeof r.leader != "string") throw new Error('MARC-in-JSON: missing or non-string "leader"');
4
+ if (!Array.isArray(r.fields)) throw new Error('MARC-in-JSON: missing or non-array "fields"');
5
+ const t = [];
6
+ for (const e of r.fields) {
7
+ if (typeof e != "object" || e === null || Array.isArray(e)) throw new Error("MARC-in-JSON: each field entry must be an object");
8
+ const o = Object.keys(e);
10
9
  if (o.length !== 1) throw new Error(`MARC-in-JSON: field entry must have exactly one key, got ${o.join(", ")}`);
11
- const n = o[0], s = t[n];
12
- if (typeof s == "string") {
13
- r.push({
10
+ const n = o[0], i = e[n];
11
+ if (typeof i == "string") {
12
+ t.push({
14
13
  tag: n,
15
- data: s
14
+ data: i
16
15
  });
17
16
  continue;
18
17
  }
19
- if (typeof s == "object" && s !== null && !Array.isArray(s)) {
20
- const a = s;
21
- if (!Array.isArray(a.subfields)) throw new Error(`MARC-in-JSON: data field "${n}" missing "subfields" array`);
22
- const y = a.subfields.map((l, c) => {
23
- if (typeof l != "object" || l === null) throw new Error(`MARC-in-JSON: subfield entry ${c} of "${n}" is not an object`);
24
- const u = Object.keys(l);
25
- if (u.length !== 1) throw new Error(`MARC-in-JSON: subfield entry ${c} of "${n}" must have exactly one key`);
26
- const f = u[0], d = l[f];
27
- if (typeof d != "string") throw new Error(`MARC-in-JSON: subfield value for "${n}$${f}" must be a string`);
18
+ if (typeof i == "object" && i !== null && !Array.isArray(i)) {
19
+ const s = i;
20
+ if (!Array.isArray(s.subfields)) throw new Error(`MARC-in-JSON: data field "${n}" missing "subfields" array`);
21
+ const y = s.subfields.map((a, u) => {
22
+ if (typeof a != "object" || a === null) throw new Error(`MARC-in-JSON: subfield entry ${u} of "${n}" is not an object`);
23
+ const l = Object.keys(a);
24
+ if (l.length !== 1) throw new Error(`MARC-in-JSON: subfield entry ${u} of "${n}" must have exactly one key`);
25
+ const c = l[0], d = a[c];
26
+ if (typeof d != "string") throw new Error(`MARC-in-JSON: subfield value for "${n}$${c}" must be a string`);
28
27
  return {
29
- code: f,
28
+ code: c,
30
29
  value: d
31
30
  };
32
31
  });
33
- r.push({
32
+ t.push({
34
33
  tag: n,
35
- indicator1: a.ind1 ?? " ",
36
- indicator2: a.ind2 ?? " ",
34
+ indicator1: s.ind1 ?? " ",
35
+ indicator2: s.ind2 ?? " ",
37
36
  subfields: y
38
37
  });
39
38
  continue;
@@ -41,35 +40,45 @@ function p(e) {
41
40
  throw new Error(`MARC-in-JSON: field "${n}" value must be a string (control) or object (data)`);
42
41
  }
43
42
  return {
44
- leader: i.leader,
45
- fields: r
43
+ leader: r.leader,
44
+ fields: t
46
45
  };
47
46
  }
48
- function g(e) {
49
- const i = e.fields.map((r) => {
50
- if (w(r)) return { [r.tag]: r.data };
51
- if (b(r)) {
52
- const t = r.subfields.map((o) => ({ [o.code]: o.value }));
53
- return { [r.tag]: {
54
- subfields: t,
55
- ind1: r.indicator1,
56
- ind2: r.indicator2
47
+ function h(r) {
48
+ if (typeof r == "string") {
49
+ const t = JSON.parse(r);
50
+ return Array.isArray(t) ? t.map(f) : [f(t)];
51
+ }
52
+ return Array.isArray(r) ? r.map(f) : [f(r)];
53
+ }
54
+ function A(r) {
55
+ const t = r.fields.map((e) => {
56
+ if (w(e)) return { [e.tag]: e.data };
57
+ if (p(e)) {
58
+ const o = e.subfields.map((n) => ({ [n.code]: n.value }));
59
+ return { [e.tag]: {
60
+ subfields: o,
61
+ ind1: e.indicator1,
62
+ ind2: e.indicator2
57
63
  } };
58
64
  }
59
65
  throw new Error("Unknown field type");
60
66
  });
61
67
  return {
62
- leader: e.leader,
63
- fields: i
68
+ leader: r.leader,
69
+ fields: t
64
70
  };
65
71
  }
66
- function A(e) {
67
- return JSON.stringify(g(e));
72
+ function b(r) {
73
+ return r.map(A);
74
+ }
75
+ function m(r) {
76
+ return JSON.stringify(b(r));
68
77
  }
69
78
  export {
70
- p as parseMarcJson,
71
- g as serializeMarcJson,
72
- A as serializeMarcJsonString
79
+ h as parseMarcJson,
80
+ b as serializeMarcJson,
81
+ m as serializeMarcJsonString
73
82
  };
74
83
 
75
84
  //# sourceMappingURL=marcjson.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"marcjson.js","names":[],"sources":["../src/marcjson.ts"],"sourcesContent":["/**\n * MARC-in-JSON parser and serializer.\n *\n * Follows the format described at https://wiki.code4lib.org/MARCJSONification\n * and used by Open Library and various REST APIs:\n *\n * {\n * \"leader\": \"01142cam a2200301 a 4500\",\n * \"fields\": [\n * { \"001\": \"5490\" },\n * { \"245\": {\n * \"subfields\": [{\"a\": \"The Hobbit\"}],\n * \"ind1\": \"1\",\n * \"ind2\": \"0\"\n * }}\n * ]\n * }\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField, isDataField } from './types';\n\n// ─── Raw JSON shape types ─────────────────────────────────────────────────────\n\nexport interface MarcJsonSubfieldEntry {\n [code: string]: string;\n}\n\nexport interface MarcJsonDataFieldValue {\n subfields: MarcJsonSubfieldEntry[];\n ind1: string;\n ind2: string;\n}\n\nexport type MarcJsonField =\n | { [tag: string]: string } // control field\n | { [tag: string]: MarcJsonDataFieldValue }; // data field\n\nexport interface MarcJsonObject {\n leader: string;\n fields: MarcJsonField[];\n}\n\n// ─── Parse ────────────────────────────────────────────────────────────────────\n\n/**\n * Parse a MARC-in-JSON object or JSON string into a MarcRecord.\n *\n * Throws on structural errors (missing `leader`, non-array `fields`,\n * unrecognised field shapes).\n */\nexport function parseMarcJson(json: string | MarcJsonObject): MarcRecord {\n const obj: MarcJsonObject = typeof json === 'string' ? JSON.parse(json) : json;\n\n if (typeof obj.leader !== 'string') {\n throw new Error('MARC-in-JSON: missing or non-string \"leader\"');\n }\n if (!Array.isArray(obj.fields)) {\n throw new Error('MARC-in-JSON: missing or non-array \"fields\"');\n }\n\n const fields: (ControlField | DataField)[] = [];\n\n for (const entry of obj.fields) {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n throw new Error('MARC-in-JSON: each field entry must be an object');\n }\n\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(`MARC-in-JSON: field entry must have exactly one key, got ${keys.join(', ')}`);\n }\n\n const tag = keys[0]!;\n const value = (entry as Record<string, unknown>)[tag];\n\n if (typeof value === 'string') {\n // Control field\n fields.push({ tag, data: value });\n continue;\n }\n\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const dv = value as MarcJsonDataFieldValue;\n if (!Array.isArray(dv.subfields)) {\n throw new Error(`MARC-in-JSON: data field \"${tag}\" missing \"subfields\" array`);\n }\n const subfields: Subfield[] = dv.subfields.map((sfEntry, idx) => {\n if (typeof sfEntry !== 'object' || sfEntry === null) {\n throw new Error(`MARC-in-JSON: subfield entry ${idx} of \"${tag}\" is not an object`);\n }\n const sfKeys = Object.keys(sfEntry);\n if (sfKeys.length !== 1) {\n throw new Error(\n `MARC-in-JSON: subfield entry ${idx} of \"${tag}\" must have exactly one key`\n );\n }\n const code = sfKeys[0]!;\n const sfValue = sfEntry[code];\n if (typeof sfValue !== 'string') {\n throw new Error(\n `MARC-in-JSON: subfield value for \"${tag}$${code}\" must be a string`\n );\n }\n return { code, value: sfValue };\n });\n\n fields.push({\n tag,\n indicator1: dv.ind1 ?? ' ',\n indicator2: dv.ind2 ?? ' ',\n subfields,\n });\n continue;\n }\n\n throw new Error(\n `MARC-in-JSON: field \"${tag}\" value must be a string (control) or object (data)`\n );\n }\n\n return { leader: obj.leader, fields };\n}\n\n// ─── Serialize ────────────────────────────────────────────────────────────────\n\n/**\n * Serialize a MarcRecord to a MARC-in-JSON plain object.\n */\nexport function serializeMarcJson(record: MarcRecord): MarcJsonObject {\n const fields: MarcJsonField[] = record.fields.map((field) => {\n if (isControlField(field)) {\n return { [field.tag]: field.data };\n }\n if (isDataField(field)) {\n const subfields: MarcJsonSubfieldEntry[] = field.subfields.map((sf) => ({\n [sf.code]: sf.value,\n }));\n return {\n [field.tag]: {\n subfields,\n ind1: field.indicator1,\n ind2: field.indicator2,\n },\n };\n }\n // Unreachable — isControlField/isDataField are exhaustive — but satisfies TS\n throw new Error('Unknown field type');\n });\n\n return { leader: record.leader, fields };\n}\n\n/**\n * Serialize a MarcRecord to a JSON string.\n */\nexport function serializeMarcJsonString(record: MarcRecord): string {\n return JSON.stringify(serializeMarcJson(record));\n}\n"],"mappings":";AAmDA,SAAgB,EAAc,GAA2C;AACvE,QAAM,IAAsB,OAAO,KAAS,WAAW,KAAK,MAAM,CAAI,IAAI;AAE1E,MAAI,OAAO,EAAI,UAAW,SACxB,OAAM,IAAI,MAAM,8CAA8C;AAEhE,MAAI,CAAC,MAAM,QAAQ,EAAI,MAAM,EAC3B,OAAM,IAAI,MAAM,6CAA6C;AAG/D,QAAM,IAAuC,CAAC;AAE9C,aAAW,KAAS,EAAI,QAAQ;AAC9B,QAAI,OAAO,KAAU,YAAY,MAAU,QAAQ,MAAM,QAAQ,CAAK,EACpE,OAAM,IAAI,MAAM,kDAAkD;AAGpE,UAAM,IAAO,OAAO,KAAK,CAAK;AAC9B,QAAI,EAAK,WAAW,EAClB,OAAM,IAAI,MAAM,4DAA4D,EAAK,KAAK,IAAI,CAAA,EAAG;AAG/F,UAAM,IAAM,EAAK,CAAA,GACX,IAAS,EAAkC,CAAA;AAEjD,QAAI,OAAO,KAAU,UAAU;AAE7B,MAAA,EAAO,KAAK;AAAA,QAAE,KAAA;AAAA,QAAK,MAAM;AAAA,MAAM,CAAC;AAChC;AAAA,IACF;AAEA,QAAI,OAAO,KAAU,YAAY,MAAU,QAAQ,CAAC,MAAM,QAAQ,CAAK,GAAG;AACxE,YAAM,IAAK;AACX,UAAI,CAAC,MAAM,QAAQ,EAAG,SAAS,EAC7B,OAAM,IAAI,MAAM,6BAA6B,CAAA,6BAAgC;AAE/E,YAAM,IAAwB,EAAG,UAAU,IAAA,CAAK,GAAS,MAAQ;AAC/D,YAAI,OAAO,KAAY,YAAY,MAAY,KAC7C,OAAM,IAAI,MAAM,gCAAgC,CAAA,QAAW,CAAA,oBAAuB;AAEpF,cAAM,IAAS,OAAO,KAAK,CAAO;AAClC,YAAI,EAAO,WAAW,EACpB,OAAM,IAAI,MACR,gCAAgC,CAAA,QAAW,CAAA,6BAC7C;AAEF,cAAM,IAAO,EAAO,CAAA,GACd,IAAU,EAAQ,CAAA;AACxB,YAAI,OAAO,KAAY,SACrB,OAAM,IAAI,MACR,qCAAqC,CAAA,IAAO,CAAA,oBAC9C;AAEF,eAAO;AAAA,UAAE,MAAA;AAAA,UAAM,OAAO;AAAA,QAAQ;AAAA,MAChC,CAAC;AAED,MAAA,EAAO,KAAK;AAAA,QACV,KAAA;AAAA,QACA,YAAY,EAAG,QAAQ;AAAA,QACvB,YAAY,EAAG,QAAQ;AAAA,QACvB,WAAA;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,UAAM,IAAI,MACR,wBAAwB,CAAA,qDAC1B;AAAA,EACF;AAEA,SAAO;AAAA,IAAE,QAAQ,EAAI;AAAA,IAAQ,QAAA;AAAA,EAAO;AACtC;AAOA,SAAgB,EAAkB,GAAoC;AACpE,QAAM,IAA0B,EAAO,OAAO,IAAA,CAAK,MAAU;AAC3D,QAAI,EAAe,CAAK,EACtB,QAAO,EAAA,CAAG,EAAM,GAAA,GAAM,EAAM,KAAK;AAEnC,QAAI,EAAY,CAAK,GAAG;AACtB,YAAM,IAAqC,EAAM,UAAU,IAAA,CAAK,OAAQ,EAAA,CACrE,EAAG,IAAA,GAAO,EAAG,MAChB,EAAE;AACF,aAAO,EAAA,CACJ,EAAM,GAAA,GAAM;AAAA,QACX,WAAA;AAAA,QACA,MAAM,EAAM;AAAA,QACZ,MAAM,EAAM;AAAA,MACd,EACF;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,oBAAoB;AAAA,EACtC,CAAC;AAED,SAAO;AAAA,IAAE,QAAQ,EAAO;AAAA,IAAQ,QAAA;AAAA,EAAO;AACzC;AAKA,SAAgB,EAAwB,GAA4B;AAClE,SAAO,KAAK,UAAU,EAAkB,CAAM,CAAC;AACjD"}
1
+ {"version":3,"file":"marcjson.js","names":[],"sources":["../src/marcjson.ts"],"sourcesContent":["/**\n * MARC-in-JSON parser and serializer.\n *\n * Follows the format described at https://wiki.code4lib.org/MARCJSONification\n * and used by Open Library and various REST APIs:\n *\n * {\n * \"leader\": \"01142cam a2200301 a 4500\",\n * \"fields\": [\n * { \"001\": \"5490\" },\n * { \"245\": {\n * \"subfields\": [{\"a\": \"The Hobbit\"}],\n * \"ind1\": \"1\",\n * \"ind2\": \"0\"\n * }}\n * ]\n * }\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField, isDataField } from './types';\n\n// ─── Raw JSON shape types ─────────────────────────────────────────────────────\n\nexport interface MarcJsonSubfieldEntry {\n [code: string]: string;\n}\n\nexport interface MarcJsonDataFieldValue {\n subfields: MarcJsonSubfieldEntry[];\n ind1: string;\n ind2: string;\n}\n\nexport type MarcJsonField =\n | { [tag: string]: string } // control field\n | { [tag: string]: MarcJsonDataFieldValue }; // data field\n\nexport interface MarcJsonObject {\n leader: string;\n fields: MarcJsonField[];\n}\n\n// ─── Parse ────────────────────────────────────────────────────────────────────\n\nfunction parseMarcJsonObject(obj: MarcJsonObject): MarcRecord {\n if (typeof obj.leader !== 'string') {\n throw new Error('MARC-in-JSON: missing or non-string \"leader\"');\n }\n if (!Array.isArray(obj.fields)) {\n throw new Error('MARC-in-JSON: missing or non-array \"fields\"');\n }\n\n const fields: (ControlField | DataField)[] = [];\n\n for (const entry of obj.fields) {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n throw new Error('MARC-in-JSON: each field entry must be an object');\n }\n\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(`MARC-in-JSON: field entry must have exactly one key, got ${keys.join(', ')}`);\n }\n\n const tag = keys[0]!;\n const value = (entry as Record<string, unknown>)[tag];\n\n if (typeof value === 'string') {\n fields.push({ tag, data: value });\n continue;\n }\n\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const dv = value as MarcJsonDataFieldValue;\n if (!Array.isArray(dv.subfields)) {\n throw new Error(`MARC-in-JSON: data field \"${tag}\" missing \"subfields\" array`);\n }\n const subfields: Subfield[] = dv.subfields.map((sfEntry, idx) => {\n if (typeof sfEntry !== 'object' || sfEntry === null) {\n throw new Error(`MARC-in-JSON: subfield entry ${idx} of \"${tag}\" is not an object`);\n }\n const sfKeys = Object.keys(sfEntry);\n if (sfKeys.length !== 1) {\n throw new Error(\n `MARC-in-JSON: subfield entry ${idx} of \"${tag}\" must have exactly one key`\n );\n }\n const code = sfKeys[0]!;\n const sfValue = sfEntry[code];\n if (typeof sfValue !== 'string') {\n throw new Error(\n `MARC-in-JSON: subfield value for \"${tag}$${code}\" must be a string`\n );\n }\n return { code, value: sfValue };\n });\n\n fields.push({\n tag,\n indicator1: dv.ind1 ?? ' ',\n indicator2: dv.ind2 ?? ' ',\n subfields,\n });\n continue;\n }\n\n throw new Error(\n `MARC-in-JSON: field \"${tag}\" value must be a string (control) or object (data)`\n );\n }\n\n return { leader: obj.leader, fields };\n}\n\n/**\n * Parse one or more MARC-in-JSON records into an array of MarcRecords.\n *\n * Accepts:\n * - A JSON string whose top-level value is an array or a single object\n * - A `MarcJsonObject[]` array\n * - A single `MarcJsonObject` (returned as a one-element array)\n *\n * Throws on structural errors (missing `leader`, non-array `fields`,\n * unrecognised field shapes).\n */\nexport function parseMarcJson(json: string | MarcJsonObject | MarcJsonObject[]): MarcRecord[] {\n if (typeof json === 'string') {\n const parsed: unknown = JSON.parse(json);\n if (Array.isArray(parsed)) {\n return (parsed as MarcJsonObject[]).map(parseMarcJsonObject);\n }\n return [parseMarcJsonObject(parsed as MarcJsonObject)];\n }\n if (Array.isArray(json)) {\n return json.map(parseMarcJsonObject);\n }\n return [parseMarcJsonObject(json)];\n}\n\n// ─── Serialize ────────────────────────────────────────────────────────────────\n\nfunction serializeMarcJsonObject(record: MarcRecord): MarcJsonObject {\n const fields: MarcJsonField[] = record.fields.map((field) => {\n if (isControlField(field)) {\n return { [field.tag]: field.data };\n }\n if (isDataField(field)) {\n const subfields: MarcJsonSubfieldEntry[] = field.subfields.map((sf) => ({\n [sf.code]: sf.value,\n }));\n return {\n [field.tag]: {\n subfields,\n ind1: field.indicator1,\n ind2: field.indicator2,\n },\n };\n }\n // Unreachable — isControlField/isDataField are exhaustive — but satisfies TS\n throw new Error('Unknown field type');\n });\n\n return { leader: record.leader, fields };\n}\n\n/**\n * Serialize an array of MarcRecords to MARC-in-JSON plain objects.\n */\nexport function serializeMarcJson(records: MarcRecord[]): MarcJsonObject[] {\n return records.map(serializeMarcJsonObject);\n}\n\n/**\n * Serialize an array of MarcRecords to a JSON string (a JSON array).\n */\nexport function serializeMarcJsonString(records: MarcRecord[]): string {\n return JSON.stringify(serializeMarcJson(records));\n}\n"],"mappings":"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"}
package/dist/marctxt.cjs CHANGED
@@ -1,8 +1,8 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("./types-CJcxHJff.cjs");function s(e){return e===" "?"\\":e}function u(e){return e==="\\"?" ":e}function l(e){return e.replace(/[{}\\\n$]/g,r=>r==="{"?"{lcub}":r==="}"?"{rcub}":r==="$"?"{dollar}":r==="\\"?"{bsol}":" ")}function f(e){return e.replace(/\{(lcub|rcub|dollar|bsol)\}/g,(r,n)=>n==="lcub"?"{":n==="rcub"?"}":n==="dollar"?"$":"\\")}function h(e){const r=e.split(/\$(.)/),n=[];for(let t=1;t<r.length;t+=2)n.push({code:r[t],value:f(r[t+1]??"")});return n}function a(e){let r="";const n=[];for(const t of e){if(!t.startsWith("="))continue;const i=t.slice(1,4),o=t.slice(6);if(i==="LDR"||i==="000"){r=o;continue}if(i<"010"){n.push({tag:i,data:f(o)});continue}const c=u(o[0]??"\\"),b=u(o[1]??"\\"),$=h(o.slice(2));n.push({tag:i,indicator1:c,indicator2:b,subfields:$})}return{leader:r,fields:n}}function d(e){const r=e.replace(/\r\n/g,`
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("./types-CJcxHJff.cjs");function u(n){return n===" "?"\\":n}function c(n){return n==="\\"?" ":n}function l(n){return n.replace(/[{}\\\n$]/g,t=>t==="{"?"{lcub}":t==="}"?"{rcub}":t==="$"?"{dollar}":t==="\\"?"{bsol}":" ")}function f(n){return n.replace(/\{(lcub|rcub|dollar|bsol)\}/g,(t,e)=>e==="lcub"?"{":e==="rcub"?"}":e==="dollar"?"$":"\\")}function $(n){const t=n.split(/\$(.)/),e=[];for(let r=1;r<t.length;r+=2)e.push({code:t[r],value:f(t[r+1]??"")});return e}function a(n){let t="";const e=[];for(const r of n){if(!r.startsWith("="))continue;const i=r.slice(1,4),o=r.slice(6);if(i==="LDR"||i==="000"){t=o;continue}if(i<"010"){e.push({tag:i,data:f(o)});continue}const s=c(o[0]??"\\"),d=c(o[1]??"\\"),p=$(o.slice(2));e.push({tag:i,indicator1:s,indicator2:d,subfields:p})}return{leader:t,fields:e}}function g(n){const t=n.replace(/\r\n/g,`
2
2
  `).split(`
3
- `),n=[];let t=[];for(const i of r)i.trim()===""?t.length>0&&(n.push(a(t)),t=[]):t.push(i);return t.length>0&&n.push(a(t)),n}function M(e){const r=d(e);if(r.length===0)throw new Error("No MARC record found in marctxt input");return r[0]}function p(e){const r=[];r.push(`=LDR ${e.leader}`);for(const n of e.fields)if(g.isControlField(n))r.push(`=${n.tag} ${l(n.data)}`);else{const t=s(n.indicator1),i=s(n.indicator2),o=n.subfields.map(c=>`$${c.code}${l(c.value)}`).join("");r.push(`=${n.tag} ${t}${i}${o}`)}return r.join(`
3
+ `),e=[];let r=[];for(const i of t)i.trim()===""?r.length>0&&(e.push(a(r)),r=[]):r.push(i);return r.length>0&&e.push(a(r)),e}function h(n){const t=[];t.push(`=LDR ${n.leader}`);for(const e of n.fields)if(b.isControlField(e))t.push(`=${e.tag} ${l(e.data)}`);else{const r=u(e.indicator1),i=u(e.indicator2),o=e.subfields.map(s=>`$${s.code}${l(s.value)}`).join("");t.push(`=${e.tag} ${r}${i}${o}`)}return t.join(`
4
4
  `)+`
5
- `}function x(e){return e.map(p).join(`
6
- `)}exports.parseMarcTxt=d;exports.parseMarcTxtRecord=M;exports.serializeMarcTxt=x;exports.serializeMarcTxtRecord=p;
5
+ `}function M(n){return n.map(h).join(`
6
+ `)}exports.parseMarcTxt=g;exports.serializeMarcTxt=M;
7
7
 
8
8
  //# sourceMappingURL=marctxt.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"marctxt.cjs","names":[],"sources":["../src/marctxt.ts"],"sourcesContent":["/**\n * MARCBreaker (marctxt) parser and serializer.\n *\n * Also known as MARCMaker format. Each field is one line:\n *\n * =LDR 00706cam a2200217 a 4500\n * =001 5490\n * =245 14$aThe Hobbit /$cJ.R.R. Tolkien.\n * =650 \\1$aHobbits (Fictitious characters)$vFiction.\n *\n * Blank indicators are represented as `\\`. Records are separated by blank lines.\n * Subfield delimiter is `$` followed by a single character code.\n *\n * Curly-brace escape sequences (per LC MARCMaker spec):\n * `{` → `{lcub}` (left curly brace; reserved by the format)\n * `}` → `{rcub}` (right curly brace; reserved by the format)\n * `$` → `{dollar}` (subfield delimiter)\n * `\\` → `{bsol}` (backslash; reserved as blank-indicator stand-in)\n *\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── Indicator encoding ───────────────────────────────────────────────────────\n\nfunction encodeIndicator(ind: string): string {\n return ind === ' ' ? '\\\\' : ind;\n}\n\nfunction decodeIndicator(ch: string): string {\n return ch === '\\\\' ? ' ' : ch;\n}\n\n// ─── Value escape (see file header) ───────────────────────────────────────────\n\nfunction escapeValue(s: string): string {\n // Single-pass replacement avoids the problem of earlier escapes being\n // re-escaped by later passes (e.g. '{' → '{lcub}' then '}' → '{lcub{rcub}').\n return s.replace(/[{}\\\\\\n$]/g, (ch) => {\n if (ch === '{') return '{lcub}';\n if (ch === '}') return '{rcub}';\n if (ch === '$') return '{dollar}';\n if (ch === '\\\\') return '{bsol}';\n return ' ';\n });\n}\n\nfunction unescapeValue(s: string): string {\n return s.replace(/\\{(lcub|rcub|dollar|bsol)\\}/g, (_, name) => {\n if (name === 'lcub') return '{';\n if (name === 'rcub') return '}';\n if (name === 'dollar') return '$';\n return '\\\\';\n });\n}\n\n// ─── Subfield parsing ─────────────────────────────────────────────────────────\n\n/**\n * Parse a subfield string like \"$aValue$bOther\" into Subfield objects.\n * Uses split with a capturing group: \"$aFoo$bBar\" → [\"\", \"a\", \"Foo\", \"b\", \"Bar\"].\n * Any character following `$` is treated as a subfield code.\n */\nfunction parseSubfields(str: string): Subfield[] {\n const parts = str.split(/\\$(.)/);\n const subfields: Subfield[] = [];\n // parts[0] is content before the first $ — should be empty for well-formed data\n for (let i = 1; i < parts.length; i += 2) {\n subfields.push({ code: parts[i]!, value: unescapeValue(parts[i + 1] ?? '') });\n }\n return subfields;\n}\n\n// ─── Record block parser ──────────────────────────────────────────────────────\n\n/**\n * Parse a block of non-empty marctxt lines into a MarcRecord.\n * Each line has the form `=TAG content`.\n */\nfunction parseRecordLines(lines: string[]): MarcRecord {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n\n for (const line of lines) {\n if (!line.startsWith('=')) continue;\n const tag = line.slice(1, 4);\n // positions 4-5 are the two separator spaces; content starts at 6\n const content = line.slice(6);\n\n if (tag === 'LDR' || tag === '000') {\n leader = content;\n continue;\n }\n\n if (tag < '010') {\n // Control field: content is the raw field data\n fields.push({ tag, data: unescapeValue(content) });\n continue;\n }\n\n // Data field: first two chars are indicators, rest are subfields\n const indicator1 = decodeIndicator(content[0] ?? '\\\\');\n const indicator2 = decodeIndicator(content[1] ?? '\\\\');\n const subfields = parseSubfields(content.slice(2));\n fields.push({ tag, indicator1, indicator2, subfields });\n }\n\n return { leader, fields };\n}\n\n// ─── Public parse API ─────────────────────────────────────────────────────────\n\n/**\n * Parse a marctxt string containing one or more records separated by blank lines.\n * Returns all records found.\n */\nexport function parseMarcTxt(text: string): MarcRecord[] {\n const lines = text.replace(/\\r\\n/g, '\\n').split('\\n');\n const records: MarcRecord[] = [];\n let buffer: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') {\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n buffer = [];\n }\n } else {\n buffer.push(line);\n }\n }\n\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n }\n\n return records;\n}\n\n/**\n * Parse a marctxt string expected to contain exactly one record.\n * Throws if no record is found.\n */\nexport function parseMarcTxtRecord(text: string): MarcRecord {\n const records = parseMarcTxt(text);\n if (records.length === 0) throw new Error('No MARC record found in marctxt input');\n return records[0]!;\n}\n\n// ─── Serializer ───────────────────────────────────────────────────────────────\n\n/**\n * Serialize a single MarcRecord to marctxt format.\n * Returns a string with one field per line and a trailing newline.\n */\nexport function serializeMarcTxtRecord(record: MarcRecord): string {\n const lines: string[] = [];\n\n lines.push(`=LDR ${record.leader}`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`=${field.tag} ${escapeValue(field.data)}`);\n } else {\n const ind1 = encodeIndicator(field.indicator1);\n const ind2 = encodeIndicator(field.indicator2);\n const subfields = field.subfields\n .map((sf) => `$${sf.code}${escapeValue(sf.value)}`)\n .join('');\n lines.push(`=${field.tag} ${ind1}${ind2}${subfields}`);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Serialize one or more MarcRecords into a marctxt string.\n * Records are separated by blank lines.\n */\nexport function serializeMarcTxt(records: MarcRecord[]): string {\n // Each record ends with '\\n'; joining with '\\n' produces blank lines between records.\n return records.map(serializeMarcTxtRecord).join('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"marctxt.cjs","names":[],"sources":["../src/marctxt.ts"],"sourcesContent":["/**\n * MARCBreaker (marctxt) parser and serializer.\n *\n * Also known as MARCMaker format. Each field is one line:\n *\n * =LDR 00706cam a2200217 a 4500\n * =001 5490\n * =245 14$aThe Hobbit /$cJ.R.R. Tolkien.\n * =650 \\1$aHobbits (Fictitious characters)$vFiction.\n *\n * Blank indicators are represented as `\\`. Records are separated by blank lines.\n * Subfield delimiter is `$` followed by a single character code.\n *\n * Curly-brace escape sequences (per LC MARCMaker spec):\n * `{` → `{lcub}` (left curly brace; reserved by the format)\n * `}` → `{rcub}` (right curly brace; reserved by the format)\n * `$` → `{dollar}` (subfield delimiter)\n * `\\` → `{bsol}` (backslash; reserved as blank-indicator stand-in)\n *\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── Indicator encoding ───────────────────────────────────────────────────────\n\nfunction encodeIndicator(ind: string): string {\n return ind === ' ' ? '\\\\' : ind;\n}\n\nfunction decodeIndicator(ch: string): string {\n return ch === '\\\\' ? ' ' : ch;\n}\n\n// ─── Value escape (see file header) ───────────────────────────────────────────\n\nfunction escapeValue(s: string): string {\n // Single-pass replacement avoids the problem of earlier escapes being\n // re-escaped by later passes (e.g. '{' → '{lcub}' then '}' → '{lcub{rcub}').\n return s.replace(/[{}\\\\\\n$]/g, (ch) => {\n if (ch === '{') return '{lcub}';\n if (ch === '}') return '{rcub}';\n if (ch === '$') return '{dollar}';\n if (ch === '\\\\') return '{bsol}';\n return ' ';\n });\n}\n\nfunction unescapeValue(s: string): string {\n return s.replace(/\\{(lcub|rcub|dollar|bsol)\\}/g, (_, name) => {\n if (name === 'lcub') return '{';\n if (name === 'rcub') return '}';\n if (name === 'dollar') return '$';\n return '\\\\';\n });\n}\n\n// ─── Subfield parsing ─────────────────────────────────────────────────────────\n\n/**\n * Parse a subfield string like \"$aValue$bOther\" into Subfield objects.\n * Uses split with a capturing group: \"$aFoo$bBar\" → [\"\", \"a\", \"Foo\", \"b\", \"Bar\"].\n * Any character following `$` is treated as a subfield code.\n */\nfunction parseSubfields(str: string): Subfield[] {\n const parts = str.split(/\\$(.)/);\n const subfields: Subfield[] = [];\n // parts[0] is content before the first $ — should be empty for well-formed data\n for (let i = 1; i < parts.length; i += 2) {\n subfields.push({ code: parts[i]!, value: unescapeValue(parts[i + 1] ?? '') });\n }\n return subfields;\n}\n\n// ─── Record block parser ──────────────────────────────────────────────────────\n\n/**\n * Parse a block of non-empty marctxt lines into a MarcRecord.\n * Each line has the form `=TAG content`.\n */\nfunction parseRecordLines(lines: string[]): MarcRecord {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n\n for (const line of lines) {\n if (!line.startsWith('=')) continue;\n const tag = line.slice(1, 4);\n // positions 4-5 are the two separator spaces; content starts at 6\n const content = line.slice(6);\n\n if (tag === 'LDR' || tag === '000') {\n leader = content;\n continue;\n }\n\n if (tag < '010') {\n // Control field: content is the raw field data\n fields.push({ tag, data: unescapeValue(content) });\n continue;\n }\n\n // Data field: first two chars are indicators, rest are subfields\n const indicator1 = decodeIndicator(content[0] ?? '\\\\');\n const indicator2 = decodeIndicator(content[1] ?? '\\\\');\n const subfields = parseSubfields(content.slice(2));\n fields.push({ tag, indicator1, indicator2, subfields });\n }\n\n return { leader, fields };\n}\n\n// ─── Public parse API ─────────────────────────────────────────────────────────\n\n/**\n * Parse a marctxt string containing one or more records separated by blank lines.\n * Returns all records found.\n */\nexport function parseMarcTxt(text: string): MarcRecord[] {\n const lines = text.replace(/\\r\\n/g, '\\n').split('\\n');\n const records: MarcRecord[] = [];\n let buffer: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') {\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n buffer = [];\n }\n } else {\n buffer.push(line);\n }\n }\n\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n }\n\n return records;\n}\n\n// ─── Serializer ───────────────────────────────────────────────────────────────\n\nfunction serializeMarcTxtRecord(record: MarcRecord): string {\n const lines: string[] = [];\n\n lines.push(`=LDR ${record.leader}`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`=${field.tag} ${escapeValue(field.data)}`);\n } else {\n const ind1 = encodeIndicator(field.indicator1);\n const ind2 = encodeIndicator(field.indicator2);\n const subfields = field.subfields\n .map((sf) => `$${sf.code}${escapeValue(sf.value)}`)\n .join('');\n lines.push(`=${field.tag} ${ind1}${ind2}${subfields}`);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Serialize one or more MarcRecords into a marctxt string.\n * Records are separated by blank lines.\n */\nexport function serializeMarcTxt(records: MarcRecord[]): string {\n // Each record ends with '\\n'; joining with '\\n' produces blank lines between records.\n return records.map(serializeMarcTxtRecord).join('\\n');\n}\n"],"mappings":"2GA0BA,SAAS,EAAgB,EAAqB,CAC5C,OAAO,IAAQ,IAAM,KAAO,CAC9B,CAEA,SAAS,EAAgB,EAAoB,CAC3C,OAAO,IAAO,KAAO,IAAM,CAC7B,CAIA,SAAS,EAAY,EAAmB,CAGtC,OAAO,EAAE,QAAQ,aAAe,GAC1B,IAAO,IAAY,SACnB,IAAO,IAAY,SACnB,IAAO,IAAY,WACnB,IAAO,KAAa,SACjB,GACR,CACH,CAEA,SAAS,EAAc,EAAmB,CACxC,OAAO,EAAE,QAAQ,+BAAA,CAAiC,EAAG,IAC/C,IAAS,OAAe,IACxB,IAAS,OAAe,IACxB,IAAS,SAAiB,IACvB,IACR,CACH,CASA,SAAS,EAAe,EAAyB,CAC/C,MAAM,EAAQ,EAAI,MAAM,OAAO,EACzB,EAAwB,CAAC,EAE/B,QAAS,EAAI,EAAG,EAAI,EAAM,OAAQ,GAAK,EACrC,EAAU,KAAK,CAAE,KAAM,EAAM,CAAA,EAAK,MAAO,EAAc,EAAM,EAAI,CAAA,GAAM,EAAE,CAAE,CAAC,EAE9E,OAAO,CACT,CAQA,SAAS,EAAiB,EAA6B,CACrD,IAAI,EAAS,GACb,MAAM,EAAuC,CAAC,EAE9C,UAAW,KAAQ,EAAO,CACxB,GAAI,CAAC,EAAK,WAAW,GAAG,EAAG,SAC3B,MAAM,EAAM,EAAK,MAAM,EAAG,CAAC,EAErB,EAAU,EAAK,MAAM,CAAC,EAE5B,GAAI,IAAQ,OAAS,IAAQ,MAAO,CAClC,EAAS,EACT,QACF,CAEA,GAAI,EAAM,MAAO,CAEf,EAAO,KAAK,CAAE,IAAA,EAAK,KAAM,EAAc,CAAO,CAAE,CAAC,EACjD,QACF,CAGA,MAAM,EAAa,EAAgB,EAAQ,CAAA,GAAM,IAAI,EAC/C,EAAa,EAAgB,EAAQ,CAAA,GAAM,IAAI,EAC/C,EAAY,EAAe,EAAQ,MAAM,CAAC,CAAC,EACjD,EAAO,KAAK,CAAE,IAAA,EAAK,WAAA,EAAY,WAAA,EAAY,UAAA,CAAU,CAAC,CACxD,CAEA,MAAO,CAAE,OAAA,EAAQ,OAAA,CAAO,CAC1B,CAQA,SAAgB,EAAa,EAA4B,CACvD,MAAM,EAAQ,EAAK,QAAQ,QAAS;AAAA,CAAI,EAAE,MAAM;AAAA,CAAI,EAC9C,EAAwB,CAAC,EAC/B,IAAI,EAAmB,CAAC,EAExB,UAAW,KAAQ,EACb,EAAK,KAAK,IAAM,GACd,EAAO,OAAS,IAClB,EAAQ,KAAK,EAAiB,CAAM,CAAC,EACrC,EAAS,CAAC,GAGZ,EAAO,KAAK,CAAI,EAIpB,OAAI,EAAO,OAAS,GAClB,EAAQ,KAAK,EAAiB,CAAM,CAAC,EAGhC,CACT,CAIA,SAAS,EAAuB,EAA4B,CAC1D,MAAM,EAAkB,CAAC,EAEzB,EAAM,KAAK,SAAS,EAAO,MAAA,EAAQ,EAEnC,UAAW,KAAS,EAAO,OACzB,GAAI,EAAA,eAAe,CAAK,EACtB,EAAM,KAAK,IAAI,EAAM,GAAA,KAAQ,EAAY,EAAM,IAAI,CAAA,EAAG,MACjD,CACL,MAAM,EAAO,EAAgB,EAAM,UAAU,EACvC,EAAO,EAAgB,EAAM,UAAU,EACvC,EAAY,EAAM,UACrB,IAAK,GAAO,IAAI,EAAG,IAAA,GAAO,EAAY,EAAG,KAAK,CAAA,EAAG,EACjD,KAAK,EAAE,EACV,EAAM,KAAK,IAAI,EAAM,GAAA,KAAQ,CAAA,GAAO,CAAA,GAAO,CAAA,EAAW,CACxD,CAGF,OAAO,EAAM,KAAK;AAAA,CAAI,EAAI;AAAA,CAC5B,CAMA,SAAgB,EAAiB,EAA+B,CAE9D,OAAO,EAAQ,IAAI,CAAsB,EAAE,KAAK;AAAA,CAAI,CACtD"}
package/dist/marctxt.d.ts CHANGED
@@ -4,16 +4,6 @@ import { MarcRecord } from './types';
4
4
  * Returns all records found.
5
5
  */
6
6
  export declare function parseMarcTxt(text: string): MarcRecord[];
7
- /**
8
- * Parse a marctxt string expected to contain exactly one record.
9
- * Throws if no record is found.
10
- */
11
- export declare function parseMarcTxtRecord(text: string): MarcRecord;
12
- /**
13
- * Serialize a single MarcRecord to marctxt format.
14
- * Returns a string with one field per line and a trailing newline.
15
- */
16
- export declare function serializeMarcTxtRecord(record: MarcRecord): string;
17
7
  /**
18
8
  * Serialize one or more MarcRecords into a marctxt string.
19
9
  * Records are separated by blank lines.
package/dist/marctxt.js CHANGED
@@ -1,43 +1,43 @@
1
1
  import { t as b } from "./types-c4Mo9m9u.js";
2
- function c(n) {
3
- return n === " " ? "\\" : n;
2
+ function u(e) {
3
+ return e === " " ? "\\" : e;
4
4
  }
5
- function u(n) {
6
- return n === "\\" ? " " : n;
5
+ function c(e) {
6
+ return e === "\\" ? " " : e;
7
7
  }
8
- function l(n) {
9
- return n.replace(/[{}\\\n$]/g, (r) => r === "{" ? "{lcub}" : r === "}" ? "{rcub}" : r === "$" ? "{dollar}" : r === "\\" ? "{bsol}" : " ");
8
+ function l(e) {
9
+ return e.replace(/[{}\\\n$]/g, (t) => t === "{" ? "{lcub}" : t === "}" ? "{rcub}" : t === "$" ? "{dollar}" : t === "\\" ? "{bsol}" : " ");
10
10
  }
11
- function a(n) {
12
- return n.replace(/\{(lcub|rcub|dollar|bsol)\}/g, (r, e) => e === "lcub" ? "{" : e === "rcub" ? "}" : e === "dollar" ? "$" : "\\");
11
+ function a(e) {
12
+ return e.replace(/\{(lcub|rcub|dollar|bsol)\}/g, (t, n) => n === "lcub" ? "{" : n === "rcub" ? "}" : n === "dollar" ? "$" : "\\");
13
13
  }
14
- function $(n) {
15
- const r = n.split(/\$(.)/), e = [];
16
- for (let t = 1; t < r.length; t += 2) e.push({
17
- code: r[t],
18
- value: a(r[t + 1] ?? "")
14
+ function $(e) {
15
+ const t = e.split(/\$(.)/), n = [];
16
+ for (let r = 1; r < t.length; r += 2) n.push({
17
+ code: t[r],
18
+ value: a(t[r + 1] ?? "")
19
19
  });
20
- return e;
20
+ return n;
21
21
  }
22
- function f(n) {
23
- let r = "";
24
- const e = [];
25
- for (const t of n) {
26
- if (!t.startsWith("=")) continue;
27
- const i = t.slice(1, 4), o = t.slice(6);
22
+ function f(e) {
23
+ let t = "";
24
+ const n = [];
25
+ for (const r of e) {
26
+ if (!r.startsWith("=")) continue;
27
+ const i = r.slice(1, 4), o = r.slice(6);
28
28
  if (i === "LDR" || i === "000") {
29
- r = o;
29
+ t = o;
30
30
  continue;
31
31
  }
32
32
  if (i < "010") {
33
- e.push({
33
+ n.push({
34
34
  tag: i,
35
35
  data: a(o)
36
36
  });
37
37
  continue;
38
38
  }
39
- const s = u(o[0] ?? "\\"), d = u(o[1] ?? "\\"), p = $(o.slice(2));
40
- e.push({
39
+ const s = c(o[0] ?? "\\"), d = c(o[1] ?? "\\"), p = $(o.slice(2));
40
+ n.push({
41
41
  tag: i,
42
42
  indicator1: s,
43
43
  indicator2: d,
@@ -45,44 +45,37 @@ function f(n) {
45
45
  });
46
46
  }
47
47
  return {
48
- leader: r,
49
- fields: e
48
+ leader: t,
49
+ fields: n
50
50
  };
51
51
  }
52
- function h(n) {
53
- const r = n.replace(/\r\n/g, `
52
+ function x(e) {
53
+ const t = e.replace(/\r\n/g, `
54
54
  `).split(`
55
- `), e = [];
56
- let t = [];
57
- for (const i of r) i.trim() === "" ? t.length > 0 && (e.push(f(t)), t = []) : t.push(i);
58
- return t.length > 0 && e.push(f(t)), e;
55
+ `), n = [];
56
+ let r = [];
57
+ for (const i of t) i.trim() === "" ? r.length > 0 && (n.push(f(r)), r = []) : r.push(i);
58
+ return r.length > 0 && n.push(f(r)), n;
59
59
  }
60
- function R(n) {
61
- const r = h(n);
62
- if (r.length === 0) throw new Error("No MARC record found in marctxt input");
63
- return r[0];
64
- }
65
- function g(n) {
66
- const r = [];
67
- r.push(`=LDR ${n.leader}`);
68
- for (const e of n.fields) if (b(e)) r.push(`=${e.tag} ${l(e.data)}`);
60
+ function g(e) {
61
+ const t = [];
62
+ t.push(`=LDR ${e.leader}`);
63
+ for (const n of e.fields) if (b(n)) t.push(`=${n.tag} ${l(n.data)}`);
69
64
  else {
70
- const t = c(e.indicator1), i = c(e.indicator2), o = e.subfields.map((s) => `$${s.code}${l(s.value)}`).join("");
71
- r.push(`=${e.tag} ${t}${i}${o}`);
65
+ const r = u(n.indicator1), i = u(n.indicator2), o = n.subfields.map((s) => `$${s.code}${l(s.value)}`).join("");
66
+ t.push(`=${n.tag} ${r}${i}${o}`);
72
67
  }
73
- return r.join(`
68
+ return t.join(`
74
69
  `) + `
75
70
  `;
76
71
  }
77
- function M(n) {
78
- return n.map(g).join(`
72
+ function R(e) {
73
+ return e.map(g).join(`
79
74
  `);
80
75
  }
81
76
  export {
82
- h as parseMarcTxt,
83
- R as parseMarcTxtRecord,
84
- M as serializeMarcTxt,
85
- g as serializeMarcTxtRecord
77
+ x as parseMarcTxt,
78
+ R as serializeMarcTxt
86
79
  };
87
80
 
88
81
  //# sourceMappingURL=marctxt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"marctxt.js","names":[],"sources":["../src/marctxt.ts"],"sourcesContent":["/**\n * MARCBreaker (marctxt) parser and serializer.\n *\n * Also known as MARCMaker format. Each field is one line:\n *\n * =LDR 00706cam a2200217 a 4500\n * =001 5490\n * =245 14$aThe Hobbit /$cJ.R.R. Tolkien.\n * =650 \\1$aHobbits (Fictitious characters)$vFiction.\n *\n * Blank indicators are represented as `\\`. Records are separated by blank lines.\n * Subfield delimiter is `$` followed by a single character code.\n *\n * Curly-brace escape sequences (per LC MARCMaker spec):\n * `{` → `{lcub}` (left curly brace; reserved by the format)\n * `}` → `{rcub}` (right curly brace; reserved by the format)\n * `$` → `{dollar}` (subfield delimiter)\n * `\\` → `{bsol}` (backslash; reserved as blank-indicator stand-in)\n *\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── Indicator encoding ───────────────────────────────────────────────────────\n\nfunction encodeIndicator(ind: string): string {\n return ind === ' ' ? '\\\\' : ind;\n}\n\nfunction decodeIndicator(ch: string): string {\n return ch === '\\\\' ? ' ' : ch;\n}\n\n// ─── Value escape (see file header) ───────────────────────────────────────────\n\nfunction escapeValue(s: string): string {\n // Single-pass replacement avoids the problem of earlier escapes being\n // re-escaped by later passes (e.g. '{' → '{lcub}' then '}' → '{lcub{rcub}').\n return s.replace(/[{}\\\\\\n$]/g, (ch) => {\n if (ch === '{') return '{lcub}';\n if (ch === '}') return '{rcub}';\n if (ch === '$') return '{dollar}';\n if (ch === '\\\\') return '{bsol}';\n return ' ';\n });\n}\n\nfunction unescapeValue(s: string): string {\n return s.replace(/\\{(lcub|rcub|dollar|bsol)\\}/g, (_, name) => {\n if (name === 'lcub') return '{';\n if (name === 'rcub') return '}';\n if (name === 'dollar') return '$';\n return '\\\\';\n });\n}\n\n// ─── Subfield parsing ─────────────────────────────────────────────────────────\n\n/**\n * Parse a subfield string like \"$aValue$bOther\" into Subfield objects.\n * Uses split with a capturing group: \"$aFoo$bBar\" → [\"\", \"a\", \"Foo\", \"b\", \"Bar\"].\n * Any character following `$` is treated as a subfield code.\n */\nfunction parseSubfields(str: string): Subfield[] {\n const parts = str.split(/\\$(.)/);\n const subfields: Subfield[] = [];\n // parts[0] is content before the first $ — should be empty for well-formed data\n for (let i = 1; i < parts.length; i += 2) {\n subfields.push({ code: parts[i]!, value: unescapeValue(parts[i + 1] ?? '') });\n }\n return subfields;\n}\n\n// ─── Record block parser ──────────────────────────────────────────────────────\n\n/**\n * Parse a block of non-empty marctxt lines into a MarcRecord.\n * Each line has the form `=TAG content`.\n */\nfunction parseRecordLines(lines: string[]): MarcRecord {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n\n for (const line of lines) {\n if (!line.startsWith('=')) continue;\n const tag = line.slice(1, 4);\n // positions 4-5 are the two separator spaces; content starts at 6\n const content = line.slice(6);\n\n if (tag === 'LDR' || tag === '000') {\n leader = content;\n continue;\n }\n\n if (tag < '010') {\n // Control field: content is the raw field data\n fields.push({ tag, data: unescapeValue(content) });\n continue;\n }\n\n // Data field: first two chars are indicators, rest are subfields\n const indicator1 = decodeIndicator(content[0] ?? '\\\\');\n const indicator2 = decodeIndicator(content[1] ?? '\\\\');\n const subfields = parseSubfields(content.slice(2));\n fields.push({ tag, indicator1, indicator2, subfields });\n }\n\n return { leader, fields };\n}\n\n// ─── Public parse API ─────────────────────────────────────────────────────────\n\n/**\n * Parse a marctxt string containing one or more records separated by blank lines.\n * Returns all records found.\n */\nexport function parseMarcTxt(text: string): MarcRecord[] {\n const lines = text.replace(/\\r\\n/g, '\\n').split('\\n');\n const records: MarcRecord[] = [];\n let buffer: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') {\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n buffer = [];\n }\n } else {\n buffer.push(line);\n }\n }\n\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n }\n\n return records;\n}\n\n/**\n * Parse a marctxt string expected to contain exactly one record.\n * Throws if no record is found.\n */\nexport function parseMarcTxtRecord(text: string): MarcRecord {\n const records = parseMarcTxt(text);\n if (records.length === 0) throw new Error('No MARC record found in marctxt input');\n return records[0]!;\n}\n\n// ─── Serializer ───────────────────────────────────────────────────────────────\n\n/**\n * Serialize a single MarcRecord to marctxt format.\n * Returns a string with one field per line and a trailing newline.\n */\nexport function serializeMarcTxtRecord(record: MarcRecord): string {\n const lines: string[] = [];\n\n lines.push(`=LDR ${record.leader}`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`=${field.tag} ${escapeValue(field.data)}`);\n } else {\n const ind1 = encodeIndicator(field.indicator1);\n const ind2 = encodeIndicator(field.indicator2);\n const subfields = field.subfields\n .map((sf) => `$${sf.code}${escapeValue(sf.value)}`)\n .join('');\n lines.push(`=${field.tag} ${ind1}${ind2}${subfields}`);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Serialize one or more MarcRecords into a marctxt string.\n * Records are separated by blank lines.\n */\nexport function serializeMarcTxt(records: MarcRecord[]): string {\n // Each record ends with '\\n'; joining with '\\n' produces blank lines between records.\n return records.map(serializeMarcTxtRecord).join('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"marctxt.js","names":[],"sources":["../src/marctxt.ts"],"sourcesContent":["/**\n * MARCBreaker (marctxt) parser and serializer.\n *\n * Also known as MARCMaker format. Each field is one line:\n *\n * =LDR 00706cam a2200217 a 4500\n * =001 5490\n * =245 14$aThe Hobbit /$cJ.R.R. Tolkien.\n * =650 \\1$aHobbits (Fictitious characters)$vFiction.\n *\n * Blank indicators are represented as `\\`. Records are separated by blank lines.\n * Subfield delimiter is `$` followed by a single character code.\n *\n * Curly-brace escape sequences (per LC MARCMaker spec):\n * `{` → `{lcub}` (left curly brace; reserved by the format)\n * `}` → `{rcub}` (right curly brace; reserved by the format)\n * `$` → `{dollar}` (subfield delimiter)\n * `\\` → `{bsol}` (backslash; reserved as blank-indicator stand-in)\n *\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── Indicator encoding ───────────────────────────────────────────────────────\n\nfunction encodeIndicator(ind: string): string {\n return ind === ' ' ? '\\\\' : ind;\n}\n\nfunction decodeIndicator(ch: string): string {\n return ch === '\\\\' ? ' ' : ch;\n}\n\n// ─── Value escape (see file header) ───────────────────────────────────────────\n\nfunction escapeValue(s: string): string {\n // Single-pass replacement avoids the problem of earlier escapes being\n // re-escaped by later passes (e.g. '{' → '{lcub}' then '}' → '{lcub{rcub}').\n return s.replace(/[{}\\\\\\n$]/g, (ch) => {\n if (ch === '{') return '{lcub}';\n if (ch === '}') return '{rcub}';\n if (ch === '$') return '{dollar}';\n if (ch === '\\\\') return '{bsol}';\n return ' ';\n });\n}\n\nfunction unescapeValue(s: string): string {\n return s.replace(/\\{(lcub|rcub|dollar|bsol)\\}/g, (_, name) => {\n if (name === 'lcub') return '{';\n if (name === 'rcub') return '}';\n if (name === 'dollar') return '$';\n return '\\\\';\n });\n}\n\n// ─── Subfield parsing ─────────────────────────────────────────────────────────\n\n/**\n * Parse a subfield string like \"$aValue$bOther\" into Subfield objects.\n * Uses split with a capturing group: \"$aFoo$bBar\" → [\"\", \"a\", \"Foo\", \"b\", \"Bar\"].\n * Any character following `$` is treated as a subfield code.\n */\nfunction parseSubfields(str: string): Subfield[] {\n const parts = str.split(/\\$(.)/);\n const subfields: Subfield[] = [];\n // parts[0] is content before the first $ — should be empty for well-formed data\n for (let i = 1; i < parts.length; i += 2) {\n subfields.push({ code: parts[i]!, value: unescapeValue(parts[i + 1] ?? '') });\n }\n return subfields;\n}\n\n// ─── Record block parser ──────────────────────────────────────────────────────\n\n/**\n * Parse a block of non-empty marctxt lines into a MarcRecord.\n * Each line has the form `=TAG content`.\n */\nfunction parseRecordLines(lines: string[]): MarcRecord {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n\n for (const line of lines) {\n if (!line.startsWith('=')) continue;\n const tag = line.slice(1, 4);\n // positions 4-5 are the two separator spaces; content starts at 6\n const content = line.slice(6);\n\n if (tag === 'LDR' || tag === '000') {\n leader = content;\n continue;\n }\n\n if (tag < '010') {\n // Control field: content is the raw field data\n fields.push({ tag, data: unescapeValue(content) });\n continue;\n }\n\n // Data field: first two chars are indicators, rest are subfields\n const indicator1 = decodeIndicator(content[0] ?? '\\\\');\n const indicator2 = decodeIndicator(content[1] ?? '\\\\');\n const subfields = parseSubfields(content.slice(2));\n fields.push({ tag, indicator1, indicator2, subfields });\n }\n\n return { leader, fields };\n}\n\n// ─── Public parse API ─────────────────────────────────────────────────────────\n\n/**\n * Parse a marctxt string containing one or more records separated by blank lines.\n * Returns all records found.\n */\nexport function parseMarcTxt(text: string): MarcRecord[] {\n const lines = text.replace(/\\r\\n/g, '\\n').split('\\n');\n const records: MarcRecord[] = [];\n let buffer: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') {\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n buffer = [];\n }\n } else {\n buffer.push(line);\n }\n }\n\n if (buffer.length > 0) {\n records.push(parseRecordLines(buffer));\n }\n\n return records;\n}\n\n// ─── Serializer ───────────────────────────────────────────────────────────────\n\nfunction serializeMarcTxtRecord(record: MarcRecord): string {\n const lines: string[] = [];\n\n lines.push(`=LDR ${record.leader}`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`=${field.tag} ${escapeValue(field.data)}`);\n } else {\n const ind1 = encodeIndicator(field.indicator1);\n const ind2 = encodeIndicator(field.indicator2);\n const subfields = field.subfields\n .map((sf) => `$${sf.code}${escapeValue(sf.value)}`)\n .join('');\n lines.push(`=${field.tag} ${ind1}${ind2}${subfields}`);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Serialize one or more MarcRecords into a marctxt string.\n * Records are separated by blank lines.\n */\nexport function serializeMarcTxt(records: MarcRecord[]): string {\n // Each record ends with '\\n'; joining with '\\n' produces blank lines between records.\n return records.map(serializeMarcTxtRecord).join('\\n');\n}\n"],"mappings":"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"}
package/dist/marcxml.cjs CHANGED
@@ -1,8 +1,8 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const X=require("./types-CJcxHJff.cjs");var b=new Map([["amp","&"],["lt","<"],["gt",">"],["quot",'"'],["apos","'"]]);function g(t){return t.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g,(r,i,c,e)=>{if(i!==void 0){const n=parseInt(i,16);return n>=0&&n<=1114111?String.fromCodePoint(n):"�"}if(c!==void 0){const n=parseInt(c,10);return n>=0&&n<=1114111?String.fromCodePoint(n):"�"}return b.get(e)??r})}function u(t){return t.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"�").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/\r/g,"&#13;")}function f(t){const r=t.indexOf(":");return r===-1?t:t.slice(r+1)}function m(t){const r={},i=/([a-zA-Z_:][^\s=]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;let c;for(;(c=i.exec(t))!==null;){const e=f(c[1]);r[e]=g(c[2]??c[3]??"")}return r}function v(t){const r=[];let i=0;for(;i<t.length;){const c=t.indexOf("<",i);if(c===-1){t.slice(i).trim()&&r.push({type:"text",text:g(t.slice(i))});break}if(c>i){const o=t.slice(i,c);o.trim()&&r.push({type:"text",text:g(o)})}const e=t.indexOf(">",c);if(e===-1)break;const n=t.slice(c+1,e);if(n.startsWith("!")||n.startsWith("?")){i=e+1;continue}if(n.startsWith("/"))r.push({type:"close",name:f(n.slice(1).trim())});else if(n.endsWith("/")){const o=n.slice(0,-1).trim(),s=o.search(/\s/),l=s===-1?o:o.slice(0,s),p=s===-1?"":o.slice(s);r.push({type:"self-close",name:f(l),attrs:m(p)})}else{const o=n.search(/\s/),s=o===-1?n:n.slice(0,o),l=o===-1?"":n.slice(o);r.push({type:"open",name:f(s),attrs:m(l)})}i=e+1}return r}function w(t,r){let i="";const c=[];let e=r;for(;e<t.length;){const n=t[e];if(n.type==="close"&&n.name==="record")return{record:{leader:i,fields:c},end:e+1};if(n.type==="open"&&n.name==="leader"){e++,e<t.length&&t[e].type==="text"&&(i=t[e].text.trim(),e++),e<t.length&&t[e].type==="close"&&e++;continue}if(n.type==="self-close"&&n.name==="controlfield"){c.push({tag:n.attrs?.tag??"",data:""}),e++;continue}if(n.type==="open"&&n.name==="controlfield"){const o=n.attrs?.tag??"";e++;let s="";e<t.length&&t[e].type==="text"&&(s=t[e].text??"",e++),e<t.length&&t[e].type==="close"&&e++,c.push({tag:o,data:s});continue}if(n.type==="self-close"&&n.name==="datafield"){c.push({tag:n.attrs?.tag??"",indicator1:n.attrs?.ind1??" ",indicator2:n.attrs?.ind2??" ",subfields:[]}),e++;continue}if(n.type==="open"&&n.name==="datafield"){const o=n.attrs?.tag??"",s=n.attrs?.ind1??" ",l=n.attrs?.ind2??" ",p=[];for(e++;e<t.length;){const d=t[e];if(d.type==="close"&&d.name==="datafield"){e++;break}if(d.type==="open"&&d.name==="subfield"){const $=d.attrs?.code??"";e++;let h="";e<t.length&&t[e].type==="text"&&(h=t[e].text??"",e++),e<t.length&&t[e].type==="close"&&e++,p.push({code:$,value:h});continue}e++}c.push({tag:o,indicator1:s,indicator2:l,subfields:p});continue}e++}return{record:{leader:i,fields:c},end:e}}function y(t){const r=v(t),i=[];let c=0;for(;c<r.length;){const e=r[c];if(e.type==="open"&&e.name==="record"){const{record:n,end:o}=w(r,c+1);i.push(n),c=o;continue}c++}return i}function A(t){const r=y(t);if(r.length===0)throw new Error("No MARC record found in MARCXML input");return r[0]}var C=`<?xml version="1.0" encoding="UTF-8"?>
2
- `,x='xmlns="http://www.loc.gov/MARC21/slim"',a=" ";function M(t){const r=[`<record ${x}>`];r.push(`${a}<leader>${u(t.leader)}</leader>`);for(const i of t.fields)if(X.isControlField(i))r.push(`${a}<controlfield tag="${i.tag}">${u(i.data)}</controlfield>`);else{const c=i.indicator1===" "?" ":i.indicator1,e=i.indicator2===" "?" ":i.indicator2;r.push(`${a}<datafield tag="${i.tag}" ind1="${c}" ind2="${e}">`);for(const n of i.subfields)r.push(`${a}${a}<subfield code="${n.code}">${u(n.value)}</subfield>`);r.push(`${a}</datafield>`)}return r.push("</record>"),r.join(`
3
- `)}function R(t){const r=[C,`<collection ${x}>`];for(const i of t){const c=M(i).split(`
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const $=require("./types-CJcxHJff.cjs");var b=new Map([["amp","&"],["lt","<"],["gt",">"],["quot",'"'],["apos","'"]]);function g(t){return t.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g,(i,r,s,e)=>{if(r!==void 0){const n=parseInt(r,16);return n>=0&&n<=1114111?String.fromCodePoint(n):"�"}if(s!==void 0){const n=parseInt(s,10);return n>=0&&n<=1114111?String.fromCodePoint(n):"�"}return b.get(e)??i})}function u(t){return t.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"�").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/\r/g,"&#13;")}function f(t){const i=t.indexOf(":");return i===-1?t:t.slice(i+1)}function m(t){const i={},r=/([a-zA-Z_:][^\s=]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;let s;for(;(s=r.exec(t))!==null;){const e=f(s[1]);i[e]=g(s[2]??s[3]??"")}return i}function v(t){const i=[];let r=0;for(;r<t.length;){const s=t.indexOf("<",r);if(s===-1){t.slice(r).trim()&&i.push({type:"text",text:g(t.slice(r))});break}if(s>r){const c=t.slice(r,s);c.trim()&&i.push({type:"text",text:g(c)})}const e=t.indexOf(">",s);if(e===-1)break;const n=t.slice(s+1,e);if(n.startsWith("!")||n.startsWith("?")){r=e+1;continue}if(n.startsWith("/"))i.push({type:"close",name:f(n.slice(1).trim())});else if(n.endsWith("/")){const c=n.slice(0,-1).trim(),o=c.search(/\s/),l=o===-1?c:c.slice(0,o),p=o===-1?"":c.slice(o);i.push({type:"self-close",name:f(l),attrs:m(p)})}else{const c=n.search(/\s/),o=c===-1?n:n.slice(0,c),l=c===-1?"":n.slice(c);i.push({type:"open",name:f(o),attrs:m(l)})}r=e+1}return i}function M(t,i){let r="";const s=[];let e=i;for(;e<t.length;){const n=t[e];if(n.type==="close"&&n.name==="record")return{record:{leader:r,fields:s},end:e+1};if(n.type==="open"&&n.name==="leader"){e++,e<t.length&&t[e].type==="text"&&(r=t[e].text.trim(),e++),e<t.length&&t[e].type==="close"&&e++;continue}if(n.type==="self-close"&&n.name==="controlfield"){s.push({tag:n.attrs?.tag??"",data:""}),e++;continue}if(n.type==="open"&&n.name==="controlfield"){const c=n.attrs?.tag??"";e++;let o="";e<t.length&&t[e].type==="text"&&(o=t[e].text??"",e++),e<t.length&&t[e].type==="close"&&e++,s.push({tag:c,data:o});continue}if(n.type==="self-close"&&n.name==="datafield"){s.push({tag:n.attrs?.tag??"",indicator1:n.attrs?.ind1??" ",indicator2:n.attrs?.ind2??" ",subfields:[]}),e++;continue}if(n.type==="open"&&n.name==="datafield"){const c=n.attrs?.tag??"",o=n.attrs?.ind1??" ",l=n.attrs?.ind2??" ",p=[];for(e++;e<t.length;){const d=t[e];if(d.type==="close"&&d.name==="datafield"){e++;break}if(d.type==="open"&&d.name==="subfield"){const x=d.attrs?.code??"";e++;let h="";e<t.length&&t[e].type==="text"&&(h=t[e].text??"",e++),e<t.length&&t[e].type==="close"&&e++,p.push({code:x,value:h});continue}e++}s.push({tag:c,indicator1:o,indicator2:l,subfields:p});continue}e++}return{record:{leader:r,fields:s},end:e}}function X(t){const i=v(t),r=[];let s=0;for(;s<i.length;){const e=i[s];if(e.type==="open"&&e.name==="record"){const{record:n,end:c}=M(i,s+1);r.push(n),s=c;continue}s++}return r}var w=`<?xml version="1.0" encoding="UTF-8"?>
2
+ `,y='xmlns="http://www.loc.gov/MARC21/slim"',a=" ";function A(t){const i=[`<record ${y}>`];i.push(`${a}<leader>${u(t.leader)}</leader>`);for(const r of t.fields)if($.isControlField(r))i.push(`${a}<controlfield tag="${r.tag}">${u(r.data)}</controlfield>`);else{const s=r.indicator1===" "?" ":r.indicator1,e=r.indicator2===" "?" ":r.indicator2;i.push(`${a}<datafield tag="${r.tag}" ind1="${s}" ind2="${e}">`);for(const n of r.subfields)i.push(`${a}${a}<subfield code="${n.code}">${u(n.value)}</subfield>`);i.push(`${a}</datafield>`)}return i.push("</record>"),i.join(`
3
+ `)}function C(t){const i=[w,`<collection ${y}>`];for(const r of t){const s=A(r).split(`
4
4
  `).map(e=>a+e).join(`
5
- `);r.push(c)}return r.push("</collection>"),r.join(`
6
- `)}exports.parseMarcXml=y;exports.parseMarcXmlRecord=A;exports.serializeMarcXml=R;exports.serializeMarcXmlRecord=M;
5
+ `);i.push(s)}return i.push("</collection>"),i.join(`
6
+ `)}exports.parseMarcXml=X;exports.serializeMarcXml=C;
7
7
 
8
8
  //# sourceMappingURL=marcxml.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"marcxml.cjs","names":[],"sources":["../src/marcxml.ts"],"sourcesContent":["/**\n * MARCXML parser and serializer.\n *\n * Supports the Library of Congress MARCXML schema:\n * http://www.loc.gov/MARC21/slim\n *\n * Parsing is done with a hand-rolled state machine — no XML library needed.\n * The MARCXML format is sufficiently regular (fixed element names, no arbitrary\n * nesting) that a full DOM parser is unnecessary.\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── XML entity handling ─────────────────────────────────────────────────────\n\nconst ENTITY_MAP: ReadonlyMap<string, string> = new Map([\n ['amp', '&'],\n ['lt', '<'],\n ['gt', '>'],\n ['quot', '\"'],\n ['apos', \"'\"],\n]);\n\nfunction unescapeXml(text: string): string {\n return text.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g, (_, hex, dec, name) => {\n if (hex !== undefined) {\n const cp = parseInt(hex, 16);\n return cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';\n }\n if (dec !== undefined) {\n const cp = parseInt(dec, 10);\n return cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';\n }\n return ENTITY_MAP.get(name) ?? _;\n });\n}\n\nfunction escapeXml(text: string): string {\n return text\n // XML 1.0 forbids most C0 control characters in document text. There is no\n // valid XML 1.0 representation for them, so substitute the Unicode\n // replacement character to keep the output well-formed.\n .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '�')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n // Preserve literal CR through the XML round-trip: XML parsers normalize\n // bare \\r and \\r\\n to \\n, so we must encode CR as a numeric reference.\n .replace(/\\r/g, '&#13;');\n}\n\n// ─── Minimal tokeniser ────────────────────────────────────────────────────────\n\ninterface Token {\n type: 'open' | 'close' | 'self-close' | 'text';\n /** Local name (no namespace prefix) */\n name?: string;\n attrs?: Record<string, string>;\n text?: string;\n}\n\n/**\n * Strip namespace prefix from a tag name, e.g. \"marc:record\" → \"record\".\n */\nfunction localName(raw: string): string {\n const colon = raw.indexOf(':');\n return colon === -1 ? raw : raw.slice(colon + 1);\n}\n\n/**\n * Parse `key=\"value\"` pairs out of an attribute string.\n */\nfunction parseAttrs(attrStr: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n const re = /([a-zA-Z_:][^\\s=]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(attrStr)) !== null) {\n const key = localName(m[1]!);\n attrs[key] = unescapeXml(m[2] ?? m[3] ?? '');\n }\n return attrs;\n}\n\n/**\n * Tokenise an XML string into a flat stream of open/close/text tokens.\n * Skips processing instructions, comments, and DOCTYPE declarations.\n * Sufficient for the well-constrained MARCXML format.\n */\nfunction tokenise(xml: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n while (i < xml.length) {\n const ltPos = xml.indexOf('<', i);\n\n // Text node before next tag\n if (ltPos === -1) {\n const text = xml.slice(i).trim();\n if (text) tokens.push({ type: 'text', text: unescapeXml(xml.slice(i)) });\n break;\n }\n\n if (ltPos > i) {\n const raw = xml.slice(i, ltPos);\n const text = raw.trim();\n if (text) tokens.push({ type: 'text', text: unescapeXml(raw) });\n }\n\n const gtPos = xml.indexOf('>', ltPos);\n if (gtPos === -1) break;\n\n const tag = xml.slice(ltPos + 1, gtPos);\n\n // Skip comments, PIs, DOCTYPE\n if (tag.startsWith('!') || tag.startsWith('?')) {\n i = gtPos + 1;\n continue;\n }\n\n if (tag.startsWith('/')) {\n tokens.push({ type: 'close', name: localName(tag.slice(1).trim()) });\n } else if (tag.endsWith('/')) {\n const inner = tag.slice(0, -1).trim();\n const spaceIdx = inner.search(/\\s/);\n const name = spaceIdx === -1 ? inner : inner.slice(0, spaceIdx);\n const attrStr = spaceIdx === -1 ? '' : inner.slice(spaceIdx);\n tokens.push({ type: 'self-close', name: localName(name), attrs: parseAttrs(attrStr) });\n } else {\n const spaceIdx = tag.search(/\\s/);\n const name = spaceIdx === -1 ? tag : tag.slice(0, spaceIdx);\n const attrStr = spaceIdx === -1 ? '' : tag.slice(spaceIdx);\n tokens.push({ type: 'open', name: localName(name), attrs: parseAttrs(attrStr) });\n }\n\n i = gtPos + 1;\n }\n\n return tokens;\n}\n\n// ─── MARCXML parser ───────────────────────────────────────────────────────────\n\n/**\n * Parse one `<record>` element's worth of tokens into a MarcRecord.\n * Mutates `pos` via the returned index.\n */\nfunction parseRecordTokens(tokens: Token[], start: number): { record: MarcRecord; end: number } {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n let i = start;\n\n while (i < tokens.length) {\n const tok = tokens[i]!;\n\n if (tok.type === 'close' && tok.name === 'record') {\n return { record: { leader, fields }, end: i + 1 };\n }\n\n if (tok.type === 'open' && tok.name === 'leader') {\n i++;\n if (i < tokens.length && tokens[i]!.type === 'text') {\n leader = tokens[i]!.text!.trim();\n i++;\n }\n // consume </leader>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n continue;\n }\n\n if (tok.type === 'self-close' && tok.name === 'controlfield') {\n fields.push({ tag: tok.attrs?.['tag'] ?? '', data: '' });\n i++;\n continue;\n }\n\n if (tok.type === 'open' && tok.name === 'controlfield') {\n const tag = tok.attrs?.['tag'] ?? '';\n i++;\n let data = '';\n if (i < tokens.length && tokens[i]!.type === 'text') {\n data = tokens[i]!.text ?? '';\n i++;\n }\n // consume </controlfield>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n fields.push({ tag, data });\n continue;\n }\n\n if (tok.type === 'self-close' && tok.name === 'datafield') {\n fields.push({\n tag: tok.attrs?.['tag'] ?? '',\n indicator1: tok.attrs?.['ind1'] ?? ' ',\n indicator2: tok.attrs?.['ind2'] ?? ' ',\n subfields: [],\n });\n i++;\n continue;\n }\n\n if (tok.type === 'open' && tok.name === 'datafield') {\n const tag = tok.attrs?.['tag'] ?? '';\n const indicator1 = tok.attrs?.['ind1'] ?? ' ';\n const indicator2 = tok.attrs?.['ind2'] ?? ' ';\n const subfields: Subfield[] = [];\n i++;\n\n while (i < tokens.length) {\n const stok = tokens[i]!;\n if (stok.type === 'close' && stok.name === 'datafield') {\n i++;\n break;\n }\n if (stok.type === 'open' && stok.name === 'subfield') {\n const code = stok.attrs?.['code'] ?? '';\n i++;\n let value = '';\n if (i < tokens.length && tokens[i]!.type === 'text') {\n value = tokens[i]!.text ?? '';\n i++;\n }\n // consume </subfield>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n subfields.push({ code, value });\n continue;\n }\n i++;\n }\n\n fields.push({ tag, indicator1, indicator2, subfields });\n continue;\n }\n\n i++;\n }\n\n return { record: { leader, fields }, end: i };\n}\n\n/**\n * Parse a MARCXML string containing one `<collection>` or one bare `<record>`.\n * Returns all records found.\n */\nexport function parseMarcXml(xml: string): MarcRecord[] {\n const tokens = tokenise(xml);\n const records: MarcRecord[] = [];\n let i = 0;\n\n while (i < tokens.length) {\n const tok = tokens[i]!;\n if (tok.type === 'open' && tok.name === 'record') {\n const { record, end } = parseRecordTokens(tokens, i + 1);\n records.push(record);\n i = end;\n continue;\n }\n i++;\n }\n\n return records;\n}\n\n/**\n * Parse a MARCXML string expected to contain exactly one `<record>`.\n * Throws if no record is found.\n */\nexport function parseMarcXmlRecord(xml: string): MarcRecord {\n const records = parseMarcXml(xml);\n if (records.length === 0) throw new Error('No MARC record found in MARCXML input');\n return records[0]!;\n}\n\n// ─── MARCXML serializer ───────────────────────────────────────────────────────\n\nconst XML_HEADER = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n';\nconst COLLECTION_NS = 'xmlns=\"http://www.loc.gov/MARC21/slim\"';\nconst INDENT = ' ';\n\n/**\n * Serialize a single MarcRecord to a `<record>` XML element string (no collection wrapper).\n */\nexport function serializeMarcXmlRecord(record: MarcRecord): string {\n const lines: string[] = [`<record ${COLLECTION_NS}>`];\n lines.push(`${INDENT}<leader>${escapeXml(record.leader)}</leader>`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`${INDENT}<controlfield tag=\"${field.tag}\">${escapeXml(field.data)}</controlfield>`);\n } else {\n const ind1 = field.indicator1 === ' ' ? ' ' : field.indicator1;\n const ind2 = field.indicator2 === ' ' ? ' ' : field.indicator2;\n lines.push(`${INDENT}<datafield tag=\"${field.tag}\" ind1=\"${ind1}\" ind2=\"${ind2}\">`);\n for (const sf of field.subfields) {\n lines.push(\n `${INDENT}${INDENT}<subfield code=\"${sf.code}\">${escapeXml(sf.value)}</subfield>`\n );\n }\n lines.push(`${INDENT}</datafield>`);\n }\n }\n\n lines.push('</record>');\n return lines.join('\\n');\n}\n\n/**\n * Serialize one or more MarcRecords into a MARCXML `<collection>` document.\n */\nexport function serializeMarcXml(records: MarcRecord[]): string {\n const parts: string[] = [\n XML_HEADER,\n `<collection ${COLLECTION_NS}>`,\n ];\n\n for (const record of records) {\n // Indent each record element by one level inside <collection>\n const recordXml = serializeMarcXmlRecord(record)\n .split('\\n')\n .map((line) => INDENT + line)\n .join('\\n');\n parts.push(recordXml);\n }\n\n parts.push('</collection>');\n return parts.join('\\n');\n}\n"],"mappings":"2GAgBA,IAAM,EAA0C,IAAI,IAAI,CACtD,CAAC,MAAO,GAAG,EACX,CAAC,KAAM,GAAG,EACV,CAAC,KAAM,GAAG,EACV,CAAC,OAAQ,GAAG,EACZ,CAAC,OAAQ,GAAG,CACd,CAAC,EAED,SAAS,EAAY,EAAsB,CACzC,OAAO,EAAK,QAAQ,gDAAA,CAAkD,EAAG,EAAK,EAAK,IAAS,CAC1F,GAAI,IAAQ,OAAW,CACrB,MAAM,EAAK,SAAS,EAAK,EAAE,EAC3B,OAAO,GAAM,GAAK,GAAM,QAAW,OAAO,cAAc,CAAE,EAAI,GAChE,CACA,GAAI,IAAQ,OAAW,CACrB,MAAM,EAAK,SAAS,EAAK,EAAE,EAC3B,OAAO,GAAM,GAAK,GAAM,QAAW,OAAO,cAAc,CAAE,EAAI,GAChE,CACA,OAAO,EAAW,IAAI,CAAI,GAAK,CACjC,CAAC,CACH,CAEA,SAAS,EAAU,EAAsB,CACvC,OAAO,EAIJ,QAAQ,gCAAiC,GAAG,EAC5C,QAAQ,KAAM,OAAO,EACrB,QAAQ,KAAM,MAAM,EACpB,QAAQ,KAAM,MAAM,EACpB,QAAQ,KAAM,QAAQ,EAGtB,QAAQ,MAAO,OAAO,CAC3B,CAeA,SAAS,EAAU,EAAqB,CACtC,MAAM,EAAQ,EAAI,QAAQ,GAAG,EAC7B,OAAO,IAAU,GAAK,EAAM,EAAI,MAAM,EAAQ,CAAC,CACjD,CAKA,SAAS,EAAW,EAAyC,CAC3D,MAAM,EAAgC,CAAC,EACjC,EAAK,qDACX,IAAI,EACJ,MAAQ,EAAI,EAAG,KAAK,CAAO,KAAO,MAAM,CACtC,MAAM,EAAM,EAAU,EAAE,CAAA,CAAG,EAC3B,EAAM,CAAA,EAAO,EAAY,EAAE,CAAA,GAAM,EAAE,CAAA,GAAM,EAAE,CAC7C,CACA,OAAO,CACT,CAOA,SAAS,EAAS,EAAsB,CACtC,MAAM,EAAkB,CAAC,EACzB,IAAI,EAAI,EAER,KAAO,EAAI,EAAI,QAAQ,CACrB,MAAM,EAAQ,EAAI,QAAQ,IAAK,CAAC,EAGhC,GAAI,IAAU,GAAI,CACH,EAAI,MAAM,CAAC,EAAE,KACtB,GAAM,EAAO,KAAK,CAAE,KAAM,OAAQ,KAAM,EAAY,EAAI,MAAM,CAAC,CAAC,CAAE,CAAC,EACvE,KACF,CAEA,GAAI,EAAQ,EAAG,CACb,MAAM,EAAM,EAAI,MAAM,EAAG,CAAK,EACjB,EAAI,KACb,GAAM,EAAO,KAAK,CAAE,KAAM,OAAQ,KAAM,EAAY,CAAG,CAAE,CAAC,CAChE,CAEA,MAAM,EAAQ,EAAI,QAAQ,IAAK,CAAK,EACpC,GAAI,IAAU,GAAI,MAElB,MAAM,EAAM,EAAI,MAAM,EAAQ,EAAG,CAAK,EAGtC,GAAI,EAAI,WAAW,GAAG,GAAK,EAAI,WAAW,GAAG,EAAG,CAC9C,EAAI,EAAQ,EACZ,QACF,CAEA,GAAI,EAAI,WAAW,GAAG,EACpB,EAAO,KAAK,CAAE,KAAM,QAAS,KAAM,EAAU,EAAI,MAAM,CAAC,EAAE,KAAK,CAAC,CAAE,CAAC,UAC1D,EAAI,SAAS,GAAG,EAAG,CAC5B,MAAM,EAAQ,EAAI,MAAM,EAAG,EAAE,EAAE,KAAK,EAC9B,EAAW,EAAM,OAAO,IAAI,EAC5B,EAAO,IAAa,GAAK,EAAQ,EAAM,MAAM,EAAG,CAAQ,EACxD,EAAU,IAAa,GAAK,GAAK,EAAM,MAAM,CAAQ,EAC3D,EAAO,KAAK,CAAE,KAAM,aAAc,KAAM,EAAU,CAAI,EAAG,MAAO,EAAW,CAAO,CAAE,CAAC,CACvF,KAAO,CACL,MAAM,EAAW,EAAI,OAAO,IAAI,EAC1B,EAAO,IAAa,GAAK,EAAM,EAAI,MAAM,EAAG,CAAQ,EACpD,EAAU,IAAa,GAAK,GAAK,EAAI,MAAM,CAAQ,EACzD,EAAO,KAAK,CAAE,KAAM,OAAQ,KAAM,EAAU,CAAI,EAAG,MAAO,EAAW,CAAO,CAAE,CAAC,CACjF,CAEA,EAAI,EAAQ,CACd,CAEA,OAAO,CACT,CAQA,SAAS,EAAkB,EAAiB,EAAoD,CAC9F,IAAI,EAAS,GACb,MAAM,EAAuC,CAAC,EAC9C,IAAI,EAAI,EAER,KAAO,EAAI,EAAO,QAAQ,CACxB,MAAM,EAAM,EAAO,CAAA,EAEnB,GAAI,EAAI,OAAS,SAAW,EAAI,OAAS,SACvC,MAAO,CAAE,OAAQ,CAAE,OAAA,EAAQ,OAAA,CAAO,EAAG,IAAK,EAAI,CAAE,EAGlD,GAAI,EAAI,OAAS,QAAU,EAAI,OAAS,SAAU,CAChD,IACI,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAC3C,EAAS,EAAO,CAAA,EAAI,KAAM,KAAK,EAC/B,KAGE,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAAS,IACtD,QACF,CAEA,GAAI,EAAI,OAAS,cAAgB,EAAI,OAAS,eAAgB,CAC5D,EAAO,KAAK,CAAE,IAAK,EAAI,OAAQ,KAAU,GAAI,KAAM,EAAG,CAAC,EACvD,IACA,QACF,CAEA,GAAI,EAAI,OAAS,QAAU,EAAI,OAAS,eAAgB,CACtD,MAAM,EAAM,EAAI,OAAQ,KAAU,GAClC,IACA,IAAI,EAAO,GACP,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAC3C,EAAO,EAAO,CAAA,EAAI,MAAQ,GAC1B,KAGE,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAAS,IACtD,EAAO,KAAK,CAAE,IAAA,EAAK,KAAA,CAAK,CAAC,EACzB,QACF,CAEA,GAAI,EAAI,OAAS,cAAgB,EAAI,OAAS,YAAa,CACzD,EAAO,KAAK,CACV,IAAK,EAAI,OAAQ,KAAU,GAC3B,WAAY,EAAI,OAAQ,MAAW,IACnC,WAAY,EAAI,OAAQ,MAAW,IACnC,UAAW,CAAC,CACd,CAAC,EACD,IACA,QACF,CAEA,GAAI,EAAI,OAAS,QAAU,EAAI,OAAS,YAAa,CACnD,MAAM,EAAM,EAAI,OAAQ,KAAU,GAC5B,EAAa,EAAI,OAAQ,MAAW,IACpC,EAAa,EAAI,OAAQ,MAAW,IACpC,EAAwB,CAAC,EAG/B,IAFA,IAEO,EAAI,EAAO,QAAQ,CACxB,MAAM,EAAO,EAAO,CAAA,EACpB,GAAI,EAAK,OAAS,SAAW,EAAK,OAAS,YAAa,CACtD,IACA,KACF,CACA,GAAI,EAAK,OAAS,QAAU,EAAK,OAAS,WAAY,CACpD,MAAM,EAAO,EAAK,OAAQ,MAAW,GACrC,IACA,IAAI,EAAQ,GACR,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAC3C,EAAQ,EAAO,CAAA,EAAI,MAAQ,GAC3B,KAGE,EAAI,EAAO,QAAU,EAAO,CAAA,EAAI,OAAS,SAAS,IACtD,EAAU,KAAK,CAAE,KAAA,EAAM,MAAA,CAAM,CAAC,EAC9B,QACF,CACA,GACF,CAEA,EAAO,KAAK,CAAE,IAAA,EAAK,WAAA,EAAY,WAAA,EAAY,UAAA,CAAU,CAAC,EACtD,QACF,CAEA,GACF,CAEA,MAAO,CAAE,OAAQ,CAAE,OAAA,EAAQ,OAAA,CAAO,EAAG,IAAK,CAAE,CAC9C,CAMA,SAAgB,EAAa,EAA2B,CACtD,MAAM,EAAS,EAAS,CAAG,EACrB,EAAwB,CAAC,EAC/B,IAAI,EAAI,EAER,KAAO,EAAI,EAAO,QAAQ,CACxB,MAAM,EAAM,EAAO,CAAA,EACnB,GAAI,EAAI,OAAS,QAAU,EAAI,OAAS,SAAU,CAChD,KAAM,CAAE,OAAA,EAAQ,IAAA,CAAA,EAAQ,EAAkB,EAAQ,EAAI,CAAC,EACvD,EAAQ,KAAK,CAAM,EACnB,EAAI,EACJ,QACF,CACA,GACF,CAEA,OAAO,CACT,CAMA,SAAgB,EAAmB,EAAyB,CAC1D,MAAM,EAAU,EAAa,CAAG,EAChC,GAAI,EAAQ,SAAW,EAAG,MAAM,IAAI,MAAM,uCAAuC,EACjF,OAAO,EAAQ,CAAA,CACjB,CAIA,IAAM,EAAa;AAAA,EACb,EAAgB,yCAChB,EAAS,KAKf,SAAgB,EAAuB,EAA4B,CACjE,MAAM,EAAkB,CAAC,WAAW,CAAA,GAAgB,EACpD,EAAM,KAAK,GAAG,CAAA,WAAiB,EAAU,EAAO,MAAM,CAAA,WAAY,EAElE,UAAW,KAAS,EAAO,OACzB,GAAI,EAAA,eAAe,CAAK,EACtB,EAAM,KAAK,GAAG,CAAA,sBAA4B,EAAM,GAAA,KAAQ,EAAU,EAAM,IAAI,CAAA,iBAAkB,MACzF,CACL,MAAM,EAAO,EAAM,aAAe,IAAM,IAAM,EAAM,WAC9C,EAAO,EAAM,aAAe,IAAM,IAAM,EAAM,WACpD,EAAM,KAAK,GAAG,CAAA,mBAAyB,EAAM,GAAA,WAAc,CAAA,WAAe,CAAA,IAAQ,EAClF,UAAW,KAAM,EAAM,UACrB,EAAM,KACJ,GAAG,CAAA,GAAS,CAAA,mBAAyB,EAAG,IAAA,KAAS,EAAU,EAAG,KAAK,CAAA,aACrE,EAEF,EAAM,KAAK,GAAG,CAAA,cAAoB,CACpC,CAGF,OAAA,EAAM,KAAK,WAAW,EACf,EAAM,KAAK;AAAA,CAAI,CACxB,CAKA,SAAgB,EAAiB,EAA+B,CAC9D,MAAM,EAAkB,CACtB,EACA,eAAe,CAAA,GACjB,EAEA,UAAW,KAAU,EAAS,CAE5B,MAAM,EAAY,EAAuB,CAAM,EAC5C,MAAM;AAAA,CAAI,EACV,IAAK,GAAS,EAAS,CAAI,EAC3B,KAAK;AAAA,CAAI,EACZ,EAAM,KAAK,CAAS,CACtB,CAEA,OAAA,EAAM,KAAK,eAAe,EACnB,EAAM,KAAK;AAAA,CAAI,CACxB"}
1
+ {"version":3,"file":"marcxml.cjs","names":[],"sources":["../src/marcxml.ts"],"sourcesContent":["/**\n * MARCXML parser and serializer.\n *\n * Supports the Library of Congress MARCXML schema:\n * http://www.loc.gov/MARC21/slim\n *\n * Parsing is done with a hand-rolled state machine — no XML library needed.\n * The MARCXML format is sufficiently regular (fixed element names, no arbitrary\n * nesting) that a full DOM parser is unnecessary.\n */\n\nimport type { MarcRecord, ControlField, DataField, Subfield } from './types';\nimport { isControlField } from './types';\n\n// ─── XML entity handling ─────────────────────────────────────────────────────\n\nconst ENTITY_MAP: ReadonlyMap<string, string> = new Map([\n ['amp', '&'],\n ['lt', '<'],\n ['gt', '>'],\n ['quot', '\"'],\n ['apos', \"'\"],\n]);\n\nfunction unescapeXml(text: string): string {\n return text.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g, (_, hex, dec, name) => {\n if (hex !== undefined) {\n const cp = parseInt(hex, 16);\n return cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';\n }\n if (dec !== undefined) {\n const cp = parseInt(dec, 10);\n return cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';\n }\n return ENTITY_MAP.get(name) ?? _;\n });\n}\n\nfunction escapeXml(text: string): string {\n return text\n // XML 1.0 forbids most C0 control characters in document text. There is no\n // valid XML 1.0 representation for them, so substitute the Unicode\n // replacement character to keep the output well-formed.\n .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '�')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n // Preserve literal CR through the XML round-trip: XML parsers normalize\n // bare \\r and \\r\\n to \\n, so we must encode CR as a numeric reference.\n .replace(/\\r/g, '&#13;');\n}\n\n// ─── Minimal tokeniser ────────────────────────────────────────────────────────\n\ninterface Token {\n type: 'open' | 'close' | 'self-close' | 'text';\n /** Local name (no namespace prefix) */\n name?: string;\n attrs?: Record<string, string>;\n text?: string;\n}\n\n/**\n * Strip namespace prefix from a tag name, e.g. \"marc:record\" → \"record\".\n */\nfunction localName(raw: string): string {\n const colon = raw.indexOf(':');\n return colon === -1 ? raw : raw.slice(colon + 1);\n}\n\n/**\n * Parse `key=\"value\"` pairs out of an attribute string.\n */\nfunction parseAttrs(attrStr: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n const re = /([a-zA-Z_:][^\\s=]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(attrStr)) !== null) {\n const key = localName(m[1]!);\n attrs[key] = unescapeXml(m[2] ?? m[3] ?? '');\n }\n return attrs;\n}\n\n/**\n * Tokenise an XML string into a flat stream of open/close/text tokens.\n * Skips processing instructions, comments, and DOCTYPE declarations.\n * Sufficient for the well-constrained MARCXML format.\n */\nfunction tokenise(xml: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n while (i < xml.length) {\n const ltPos = xml.indexOf('<', i);\n\n // Text node before next tag\n if (ltPos === -1) {\n const text = xml.slice(i).trim();\n if (text) tokens.push({ type: 'text', text: unescapeXml(xml.slice(i)) });\n break;\n }\n\n if (ltPos > i) {\n const raw = xml.slice(i, ltPos);\n const text = raw.trim();\n if (text) tokens.push({ type: 'text', text: unescapeXml(raw) });\n }\n\n const gtPos = xml.indexOf('>', ltPos);\n if (gtPos === -1) break;\n\n const tag = xml.slice(ltPos + 1, gtPos);\n\n // Skip comments, PIs, DOCTYPE\n if (tag.startsWith('!') || tag.startsWith('?')) {\n i = gtPos + 1;\n continue;\n }\n\n if (tag.startsWith('/')) {\n tokens.push({ type: 'close', name: localName(tag.slice(1).trim()) });\n } else if (tag.endsWith('/')) {\n const inner = tag.slice(0, -1).trim();\n const spaceIdx = inner.search(/\\s/);\n const name = spaceIdx === -1 ? inner : inner.slice(0, spaceIdx);\n const attrStr = spaceIdx === -1 ? '' : inner.slice(spaceIdx);\n tokens.push({ type: 'self-close', name: localName(name), attrs: parseAttrs(attrStr) });\n } else {\n const spaceIdx = tag.search(/\\s/);\n const name = spaceIdx === -1 ? tag : tag.slice(0, spaceIdx);\n const attrStr = spaceIdx === -1 ? '' : tag.slice(spaceIdx);\n tokens.push({ type: 'open', name: localName(name), attrs: parseAttrs(attrStr) });\n }\n\n i = gtPos + 1;\n }\n\n return tokens;\n}\n\n// ─── MARCXML parser ───────────────────────────────────────────────────────────\n\n/**\n * Parse one `<record>` element's worth of tokens into a MarcRecord.\n * Mutates `pos` via the returned index.\n */\nfunction parseRecordTokens(tokens: Token[], start: number): { record: MarcRecord; end: number } {\n let leader = '';\n const fields: (ControlField | DataField)[] = [];\n let i = start;\n\n while (i < tokens.length) {\n const tok = tokens[i]!;\n\n if (tok.type === 'close' && tok.name === 'record') {\n return { record: { leader, fields }, end: i + 1 };\n }\n\n if (tok.type === 'open' && tok.name === 'leader') {\n i++;\n if (i < tokens.length && tokens[i]!.type === 'text') {\n leader = tokens[i]!.text!.trim();\n i++;\n }\n // consume </leader>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n continue;\n }\n\n if (tok.type === 'self-close' && tok.name === 'controlfield') {\n fields.push({ tag: tok.attrs?.['tag'] ?? '', data: '' });\n i++;\n continue;\n }\n\n if (tok.type === 'open' && tok.name === 'controlfield') {\n const tag = tok.attrs?.['tag'] ?? '';\n i++;\n let data = '';\n if (i < tokens.length && tokens[i]!.type === 'text') {\n data = tokens[i]!.text ?? '';\n i++;\n }\n // consume </controlfield>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n fields.push({ tag, data });\n continue;\n }\n\n if (tok.type === 'self-close' && tok.name === 'datafield') {\n fields.push({\n tag: tok.attrs?.['tag'] ?? '',\n indicator1: tok.attrs?.['ind1'] ?? ' ',\n indicator2: tok.attrs?.['ind2'] ?? ' ',\n subfields: [],\n });\n i++;\n continue;\n }\n\n if (tok.type === 'open' && tok.name === 'datafield') {\n const tag = tok.attrs?.['tag'] ?? '';\n const indicator1 = tok.attrs?.['ind1'] ?? ' ';\n const indicator2 = tok.attrs?.['ind2'] ?? ' ';\n const subfields: Subfield[] = [];\n i++;\n\n while (i < tokens.length) {\n const stok = tokens[i]!;\n if (stok.type === 'close' && stok.name === 'datafield') {\n i++;\n break;\n }\n if (stok.type === 'open' && stok.name === 'subfield') {\n const code = stok.attrs?.['code'] ?? '';\n i++;\n let value = '';\n if (i < tokens.length && tokens[i]!.type === 'text') {\n value = tokens[i]!.text ?? '';\n i++;\n }\n // consume </subfield>\n if (i < tokens.length && tokens[i]!.type === 'close') i++;\n subfields.push({ code, value });\n continue;\n }\n i++;\n }\n\n fields.push({ tag, indicator1, indicator2, subfields });\n continue;\n }\n\n i++;\n }\n\n return { record: { leader, fields }, end: i };\n}\n\n/**\n * Parse a MARCXML string containing one `<collection>` or one bare `<record>`.\n * Returns all records found.\n */\nexport function parseMarcXml(xml: string): MarcRecord[] {\n const tokens = tokenise(xml);\n const records: MarcRecord[] = [];\n let i = 0;\n\n while (i < tokens.length) {\n const tok = tokens[i]!;\n if (tok.type === 'open' && tok.name === 'record') {\n const { record, end } = parseRecordTokens(tokens, i + 1);\n records.push(record);\n i = end;\n continue;\n }\n i++;\n }\n\n return records;\n}\n\n// ─── MARCXML serializer ───────────────────────────────────────────────────────\n\nconst XML_HEADER = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n';\nconst COLLECTION_NS = 'xmlns=\"http://www.loc.gov/MARC21/slim\"';\nconst INDENT = ' ';\n\nfunction serializeMarcXmlRecord(record: MarcRecord): string {\n const lines: string[] = [`<record ${COLLECTION_NS}>`];\n lines.push(`${INDENT}<leader>${escapeXml(record.leader)}</leader>`);\n\n for (const field of record.fields) {\n if (isControlField(field)) {\n lines.push(`${INDENT}<controlfield tag=\"${field.tag}\">${escapeXml(field.data)}</controlfield>`);\n } else {\n const ind1 = field.indicator1 === ' ' ? ' ' : field.indicator1;\n const ind2 = field.indicator2 === ' ' ? ' ' : field.indicator2;\n lines.push(`${INDENT}<datafield tag=\"${field.tag}\" ind1=\"${ind1}\" ind2=\"${ind2}\">`);\n for (const sf of field.subfields) {\n lines.push(\n `${INDENT}${INDENT}<subfield code=\"${sf.code}\">${escapeXml(sf.value)}</subfield>`\n );\n }\n lines.push(`${INDENT}</datafield>`);\n }\n }\n\n lines.push('</record>');\n return lines.join('\\n');\n}\n\n/**\n * Serialize one or more MarcRecords into a MARCXML `<collection>` document.\n */\nexport function serializeMarcXml(records: MarcRecord[]): string {\n const parts: string[] = [\n XML_HEADER,\n `<collection ${COLLECTION_NS}>`,\n ];\n\n for (const record of records) {\n // Indent each record element by one level inside <collection>\n const recordXml = serializeMarcXmlRecord(record)\n .split('\\n')\n .map((line) => INDENT + line)\n .join('\\n');\n parts.push(recordXml);\n }\n\n parts.push('</collection>');\n return parts.join('\\n');\n}\n"],"mappings":"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"}
package/dist/marcxml.d.ts CHANGED
@@ -4,15 +4,6 @@ import { MarcRecord } from './types';
4
4
  * Returns all records found.
5
5
  */
6
6
  export declare function parseMarcXml(xml: string): MarcRecord[];
7
- /**
8
- * Parse a MARCXML string expected to contain exactly one `<record>`.
9
- * Throws if no record is found.
10
- */
11
- export declare function parseMarcXmlRecord(xml: string): MarcRecord;
12
- /**
13
- * Serialize a single MarcRecord to a `<record>` XML element string (no collection wrapper).
14
- */
15
- export declare function serializeMarcXmlRecord(record: MarcRecord): string;
16
7
  /**
17
8
  * Serialize one or more MarcRecords into a MARCXML `<collection>` document.
18
9
  */