@diy-accounting-uk/diya-gl 1.2.58 → 1.2.59
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.
|
@@ -1741,6 +1741,32 @@
|
|
|
1741
1741
|
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1742
1742
|
}
|
|
1743
1743
|
}
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
"tag": "diya-gl-v1.2.58",
|
|
1747
|
+
"date": "2026-10-25",
|
|
1748
|
+
"formatVersion": "diya-gl/1",
|
|
1749
|
+
"engineVersion": "1.2.58+ad393516",
|
|
1750
|
+
"taxDataHash": "51bdff60e993",
|
|
1751
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1752
|
+
"templates": {
|
|
1753
|
+
"bst": {
|
|
1754
|
+
"hash": "0d003e10e00c",
|
|
1755
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1756
|
+
},
|
|
1757
|
+
"taxi": {
|
|
1758
|
+
"hash": "1ff69654d64d",
|
|
1759
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1760
|
+
},
|
|
1761
|
+
"se": {
|
|
1762
|
+
"hash": "064605ae76ed",
|
|
1763
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1764
|
+
},
|
|
1765
|
+
"ltd": {
|
|
1766
|
+
"hash": "a65ab09cf35c",
|
|
1767
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1744
1770
|
}
|
|
1745
1771
|
]
|
|
1746
1772
|
}
|
|
@@ -46,7 +46,7 @@ import { canonicalBookToml, canonicalLinesJsonl, compareLines, orderedBookTopLev
|
|
|
46
46
|
import { serializeReportDocument } from "./report-serializer.js";
|
|
47
47
|
import { parseDiyaGlData } from "./diya-gl-loader.js";
|
|
48
48
|
import { stampBook } from "./provenance.js";
|
|
49
|
-
import { sniffIxbrl } from "./ixbrl-reader.js";
|
|
49
|
+
import { sniffIxbrl, readIxbrlSet, checkSameReport, IxbrlError } from "./ixbrl-reader.js";
|
|
50
50
|
import { mapIxbrlAccounts } from "./ixbrl-accounts.js";
|
|
51
51
|
import { buildIxbrlSummaryBooks } from "./ixbrl-summary-book.js";
|
|
52
52
|
import * as bst from "../products/bst.js";
|
|
@@ -190,6 +190,12 @@ async function zipKind(bytes) {
|
|
|
190
190
|
const jsonEntries = entries.filter((name) => name.toLowerCase().endsWith(".json"));
|
|
191
191
|
if (jsonEntries.length === 1 && entries.length === 1) return "json-zip";
|
|
192
192
|
|
|
193
|
+
const documents = entries.filter((name) => !name.toUpperCase().startsWith("META-INF/"));
|
|
194
|
+
if (documents.length > 0) {
|
|
195
|
+
const sniffed = await Promise.all(documents.map(async (name) => sniffIxbrl(await zip.file(name).async("uint8array"))));
|
|
196
|
+
if (sniffed.every(Boolean)) return "ixbrl-zip";
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
return "unknown";
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -200,7 +206,7 @@ async function zipKind(bytes) {
|
|
|
200
206
|
* error messages only.
|
|
201
207
|
* @param {Uint8Array} bytes
|
|
202
208
|
* @param {string} [name] - a hint for messages, not for detection
|
|
203
|
-
* @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"ixbrl"|"xbrl"|"xls"|"unknown">}
|
|
209
|
+
* @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"ixbrl"|"xbrl"|"ixbrl-zip"|"xls"|"unknown">}
|
|
204
210
|
*/
|
|
205
211
|
export async function detectBookSource(bytes, name) {
|
|
206
212
|
void name; // content decides the kind; the name is a hint for a caller's own error messages, never for detection
|
|
@@ -463,8 +469,8 @@ async function readJsonZipSource(bytes) {
|
|
|
463
469
|
// An accounts file stands for two Company books, the file's own year and the
|
|
464
470
|
// year before, each a summary with one line per tagged line. The comparative
|
|
465
471
|
// pair, the accounts figures and the facts no figure uses ride beside them.
|
|
466
|
-
async function readIxbrlSource(kind,
|
|
467
|
-
const accounts = mapIxbrlAccounts(
|
|
472
|
+
async function readIxbrlSource(kind, source, deps) {
|
|
473
|
+
const accounts = mapIxbrlAccounts(source, deps.expect, { suiteConcepts: deps.suiteConcepts });
|
|
468
474
|
const summary = await buildIxbrlSummaryBooks(accounts, { resources: deps.resources });
|
|
469
475
|
const current = validated(kind, summary.book, summary.lines);
|
|
470
476
|
const prior = validated(kind, summary.comparative.book, summary.comparative.lines);
|
|
@@ -478,6 +484,67 @@ async function readIxbrlSource(kind, bytes, deps) {
|
|
|
478
484
|
};
|
|
479
485
|
}
|
|
480
486
|
|
|
487
|
+
// The directories of a Companies House package that hold the company's own
|
|
488
|
+
// accounts, in the order they are looked for.
|
|
489
|
+
const PACKAGE_ENTRY_DIRECTORIES = ["reports/english", "accounts", "subsidiary-accounts"];
|
|
490
|
+
const PACKAGE_WELSH_DIRECTORY = "reports/welsh";
|
|
491
|
+
|
|
492
|
+
const directoryOf = (entry) => entry.slice(0, Math.max(0, entry.lastIndexOf("/")));
|
|
493
|
+
|
|
494
|
+
// A zip of iXBRL documents is one document set when its files sit together,
|
|
495
|
+
// or a Companies House package when they sit in directories. A package is
|
|
496
|
+
// read through its entry directory; a Welsh package is also compared with its
|
|
497
|
+
// Welsh directory, as Companies House compares the two.
|
|
498
|
+
async function readIxbrlZipSource(bytes) {
|
|
499
|
+
const zip = await JSZip.loadAsync(bytes);
|
|
500
|
+
const entries = Object.keys(zip.files)
|
|
501
|
+
.filter((name) => !zip.files[name].dir && !name.toUpperCase().startsWith("META-INF/"))
|
|
502
|
+
.sort();
|
|
503
|
+
const documentOf = async (entry) => ({ name: entry, bytes: await zip.file(entry).async("uint8array") });
|
|
504
|
+
|
|
505
|
+
const directories = new Map();
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const directory = directoryOf(entry);
|
|
508
|
+
directories.set(directory, [...(directories.get(directory) ?? []), entry]);
|
|
509
|
+
}
|
|
510
|
+
if (directories.size === 1 && directories.has("")) return readIxbrlSet(await Promise.all(entries.map(documentOf)));
|
|
511
|
+
|
|
512
|
+
for (const [directory, members] of directories) {
|
|
513
|
+
if (members.length > 1)
|
|
514
|
+
throw new IxbrlError(`The zip holds more than one accounts file in "${directory}/"; a package folder holds one.`);
|
|
515
|
+
}
|
|
516
|
+
const entryDirectory = PACKAGE_ENTRY_DIRECTORIES.find((directory) => directories.has(directory));
|
|
517
|
+
if (!entryDirectory) {
|
|
518
|
+
throw new IxbrlError(
|
|
519
|
+
directories.size === 1
|
|
520
|
+
? "The zip holds accounts in a folder diya-gl does not recognise; drop the accounts file on its own."
|
|
521
|
+
: "The zip holds several folders and none is the company's own accounts; drop the accounts file on its own.",
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const read = readIxbrlSet([await documentOf(directories.get(entryDirectory)[0])]);
|
|
525
|
+
if (entryDirectory === "reports/english" && directories.has(PACKAGE_WELSH_DIRECTORY)) {
|
|
526
|
+
const welsh = readIxbrlSet([await documentOf(directories.get(PACKAGE_WELSH_DIRECTORY)[0])]);
|
|
527
|
+
checkSameReport(read, welsh, "English", "Welsh");
|
|
528
|
+
}
|
|
529
|
+
return read;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Turn several files dropped together into D. Files that are all iXBRL
|
|
534
|
+
* documents are one document set. One file is read as readBookSource reads it.
|
|
535
|
+
* @param {Array<{name: string, bytes: Uint8Array}>} files
|
|
536
|
+
* @param {Object} [deps] - as readBookSource
|
|
537
|
+
* @returns {Promise<Object>} as readBookSource
|
|
538
|
+
*/
|
|
539
|
+
export async function readBookSources(files, deps = {}) {
|
|
540
|
+
if (files.length === 1) return readBookSource(files[0].bytes, files[0].name, deps);
|
|
541
|
+
const kinds = await Promise.all(files.map((file) => detectBookSource(file.bytes, file.name)));
|
|
542
|
+
if (kinds.every((kind) => kind === "ixbrl" || kind === "xbrl")) {
|
|
543
|
+
return readIxbrlSource("ixbrl", readIxbrlSet(files), deps);
|
|
544
|
+
}
|
|
545
|
+
throw new IxbrlError("Drop the accounts files on their own.");
|
|
546
|
+
}
|
|
547
|
+
|
|
481
548
|
/**
|
|
482
549
|
* Turn a byte array into D: the book and its lines, whichever of the kinds
|
|
483
550
|
* it sniffs as. The CLI's --file mode, the MCP server's extract_book
|
|
@@ -508,6 +575,8 @@ export async function readBookSource(bytes, name, deps = {}) {
|
|
|
508
575
|
case "ixbrl":
|
|
509
576
|
case "xbrl":
|
|
510
577
|
return readIxbrlSource(kind, bytes, deps);
|
|
578
|
+
case "ixbrl-zip":
|
|
579
|
+
return readIxbrlSource(kind, await readIxbrlZipSource(bytes), deps);
|
|
511
580
|
case "xls":
|
|
512
581
|
throw new XlsBookSourceError(name);
|
|
513
582
|
default:
|
|
@@ -43,6 +43,8 @@ const FRC_NAMESPACE = /^http:\/\/xbrl\.frc\.org\.uk\/(fr|cd|reports)\/(\d{4}-\d{
|
|
|
43
43
|
const FRC_ENTRY_POINT = /xbrl\.frc\.org\.uk\/([^/]+)\/(\d{4}-\d{2}-\d{2})\//;
|
|
44
44
|
const OLD_UK_GAAP_NAMESPACE = /^http:\/\/www\.xbrl\.org\/uk\/(gaap|cd|fr)\b/;
|
|
45
45
|
|
|
46
|
+
const DROP_THE_SET = "Drop all the files of the set together, or the zip.";
|
|
47
|
+
|
|
46
48
|
// What a customer sees when a file reads as something other than FRS 102 or
|
|
47
49
|
// FRS 105 accounts.
|
|
48
50
|
const READ_SUITES = ["FRS-102", "FRS-105"];
|
|
@@ -361,7 +363,9 @@ function inlineParts(document) {
|
|
|
361
363
|
const root = document.documentElement;
|
|
362
364
|
const headers = descendants(root, (element) => isIx(element, "header"));
|
|
363
365
|
const resources = headers.flatMap((header) => descendants(header, (element) => isIx(element, "resources")));
|
|
364
|
-
const schemaRefs = headers.flatMap((header) =>
|
|
366
|
+
const schemaRefs = headers.flatMap((header) =>
|
|
367
|
+
descendants(header, (element) => is(element, NS_LINK, "schemaRef") && !element.parentNode.getAttribute?.("target")),
|
|
368
|
+
);
|
|
365
369
|
const hiddenElements = new Set(headers.flatMap((header) => descendants(header, (element) => isIx(element, "hidden"))));
|
|
366
370
|
const factElements = descendants(root, (element) => isIx(element, "nonFraction") || isIx(element, "nonNumeric"));
|
|
367
371
|
const continuations = new Map(
|
|
@@ -398,9 +402,7 @@ function nonNumericText(element, continuations) {
|
|
|
398
402
|
seen.add(next);
|
|
399
403
|
const continuation = continuations.get(next);
|
|
400
404
|
if (!continuation) {
|
|
401
|
-
throw new IxbrlError(
|
|
402
|
-
"This file is one part of a document set: a fact continues in another file. diya-gl reads single-file documents.",
|
|
403
|
-
);
|
|
405
|
+
throw new IxbrlError(`This file is one part of a document set: a fact continues in another file. ${DROP_THE_SET}`);
|
|
404
406
|
}
|
|
405
407
|
text += textOf(continuation);
|
|
406
408
|
next = continuation.getAttribute("continuedAt");
|
|
@@ -483,101 +485,248 @@ function checkSuite(suite) {
|
|
|
483
485
|
}
|
|
484
486
|
}
|
|
485
487
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
*/
|
|
492
|
-
export function readIxbrl(input) {
|
|
493
|
-
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
494
|
-
const kind = sniffIxbrl(bytes);
|
|
495
|
-
if (!kind) throw new IxbrlError("This file is not an Inline XBRL document or an XBRL instance.");
|
|
488
|
+
const nameOf = (name) => (name ? `"${name}"` : "This file");
|
|
489
|
+
|
|
490
|
+
function describeSuite(suite) {
|
|
491
|
+
return [suite.family, suite.date].filter(Boolean).join(" ");
|
|
492
|
+
}
|
|
496
493
|
|
|
497
|
-
|
|
498
|
-
|
|
494
|
+
function sameJson(a, b) {
|
|
495
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
496
|
+
}
|
|
499
497
|
|
|
498
|
+
function mergeResources(parsed) {
|
|
500
499
|
const contexts = {};
|
|
501
500
|
const units = {};
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
501
|
+
const identifiers = new Set();
|
|
502
|
+
for (const part of parsed) {
|
|
503
|
+
for (const resource of part.parts.resources) {
|
|
504
|
+
for (const child of elementChildren(resource)) {
|
|
505
|
+
if (is(child, NS_XBRLI, "context")) {
|
|
506
|
+
const context = readContext(child);
|
|
507
|
+
if (contexts[context.id] && !sameJson(contexts[context.id], context)) {
|
|
508
|
+
throw new IxbrlError(`The files define the context "${context.id}" differently.`);
|
|
509
|
+
}
|
|
510
|
+
contexts[context.id] = context;
|
|
511
|
+
identifiers.add(`${context.entity.scheme}|${context.entity.identifier}`);
|
|
512
|
+
} else if (is(child, NS_XBRLI, "unit")) {
|
|
513
|
+
const id = child.getAttribute("id");
|
|
514
|
+
const unit = readUnit(child);
|
|
515
|
+
if (units[id] !== undefined && units[id] !== unit) throw new IxbrlError(`The files define the unit "${id}" differently.`);
|
|
516
|
+
units[id] = unit;
|
|
517
|
+
}
|
|
509
518
|
}
|
|
510
519
|
}
|
|
511
520
|
}
|
|
512
|
-
|
|
513
|
-
|
|
521
|
+
return { contexts, units, identifiers };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function checkSetAgrees(parsed, identifiers) {
|
|
525
|
+
if (identifiers.size > 1) {
|
|
526
|
+
const names = [...identifiers].map((id) => id.split("|")[1]);
|
|
527
|
+
throw new IxbrlError(`The files are for different companies (${names.join(" and ")}).`);
|
|
528
|
+
}
|
|
529
|
+
const suites = parsed
|
|
530
|
+
.map((part) => suiteOf(new Set(), part.parts.schemaRefs))
|
|
531
|
+
.filter((suite) => suite.family || suite.date)
|
|
532
|
+
.map(describeSuite);
|
|
533
|
+
if (new Set(suites).size > 1) {
|
|
534
|
+
throw new IxbrlError(`The files name different taxonomy suites (${[...new Set(suites)].join(" and ")}).`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function readFactElement(element, part, shared) {
|
|
539
|
+
const { kind, parts } = part;
|
|
540
|
+
const { contexts, units, continuations } = shared;
|
|
541
|
+
const nameAttribute = kind === "ixbrl" ? element.getAttribute("name") : null;
|
|
542
|
+
if (kind === "ixbrl" && !nameAttribute) return null;
|
|
543
|
+
if (element.getAttributeNS(NS_XSI, "nil") === "true" || element.getAttribute("xsi:nil") === "true") return null;
|
|
544
|
+
|
|
545
|
+
const { namespace, localName } =
|
|
546
|
+
kind === "ixbrl"
|
|
547
|
+
? resolveQName(element, nameAttribute, "a concept name")
|
|
548
|
+
: { namespace: element.namespaceURI, localName: element.localName };
|
|
549
|
+
|
|
550
|
+
const contextRef = element.getAttribute("contextRef");
|
|
551
|
+
const context = contexts[contextRef];
|
|
552
|
+
if (!context) throw new IxbrlError(`The fact ${localName} refers to the context "${contextRef}", which the file does not define.`);
|
|
553
|
+
|
|
554
|
+
const numeric = kind === "ixbrl" ? isIx(element, "nonFraction") : element.hasAttribute("unitRef");
|
|
555
|
+
let value;
|
|
556
|
+
let unit = null;
|
|
557
|
+
let decimals;
|
|
558
|
+
if (numeric) {
|
|
559
|
+
const unitRef = element.getAttribute("unitRef");
|
|
560
|
+
unit = units[unitRef];
|
|
561
|
+
if (unit === undefined) throw new IxbrlError(`The fact ${localName} refers to the unit "${unitRef}", which the file does not define.`);
|
|
562
|
+
if (unit !== "GBP" && unit !== "pure" && unit !== "shares" && !unit.startsWith("divide:")) {
|
|
563
|
+
throw new IxbrlError(`The fact ${localName} is stated in ${unit}; diya-gl reads pounds sterling (GBP), shares and pure numbers.`);
|
|
564
|
+
}
|
|
565
|
+
const decimalsText = element.getAttribute("decimals");
|
|
566
|
+
decimals = decimalsText === null ? undefined : decimalsText === "INF" ? Infinity : Number(decimalsText);
|
|
567
|
+
value = kind === "ixbrl" ? nonFractionValue(element) : Number(textOf(element).trim());
|
|
568
|
+
} else {
|
|
569
|
+
value = kind === "ixbrl" ? nonNumericValue(element, continuations) : textOf(element).trim();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
namespace,
|
|
574
|
+
localName,
|
|
575
|
+
contextRef,
|
|
576
|
+
entity: context.entity,
|
|
577
|
+
period: context.period,
|
|
578
|
+
dimensions: context.dimensions,
|
|
579
|
+
unit,
|
|
580
|
+
decimals,
|
|
581
|
+
value,
|
|
582
|
+
hidden: insideHidden(element, parts.hiddenElements),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Read one or more Inline XBRL documents, or one XBRL instance, into facts.
|
|
588
|
+
* Several documents are one Inline XBRL Document Set: their contexts, units
|
|
589
|
+
* and continuations merge, facts with no target attribute are read, and the
|
|
590
|
+
* duplicate rule runs across the whole set.
|
|
591
|
+
* @param {Array<{name: string, bytes: Uint8Array|string}>} documents
|
|
592
|
+
* @returns {{kind: "ixbrl"|"xbrl", suite: {family: string|null, date: string|null, modules: string[]}, facts: Array<Object>, contexts: Object, units: Object, leftOutTargets: string[]}}
|
|
593
|
+
* @throws {IxbrlError} when the set cannot be read, with a message a customer reads
|
|
594
|
+
*/
|
|
595
|
+
export function readIxbrlSet(documents) {
|
|
596
|
+
if (!Array.isArray(documents) || documents.length === 0) throw new IxbrlError("There is no file to read.");
|
|
597
|
+
const many = documents.length > 1;
|
|
598
|
+
|
|
599
|
+
const parsed = documents.map(({ name, bytes: input }) => {
|
|
600
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
601
|
+
const kind = sniffIxbrl(bytes);
|
|
602
|
+
if (!kind) throw new IxbrlError(`${nameOf(name)} is not an Inline XBRL document or an XBRL instance.`);
|
|
603
|
+
if (many && kind === "xbrl") throw new IxbrlError(`${nameOf(name)} is an XBRL instance; an instance is one file.`);
|
|
604
|
+
const document = parseDocument(decodeUtf8(bytes));
|
|
605
|
+
return { name, kind, parts: kind === "ixbrl" ? inlineParts(document) : instanceParts(document) };
|
|
606
|
+
});
|
|
607
|
+
const kind = parsed[0].kind;
|
|
608
|
+
|
|
609
|
+
const { contexts, units, identifiers } = mergeResources(parsed);
|
|
610
|
+
if (many) checkSetAgrees(parsed, identifiers);
|
|
611
|
+
|
|
612
|
+
const continuations = new Map();
|
|
613
|
+
for (const part of parsed) for (const [id, element] of part.parts.continuations) continuations.set(id, element);
|
|
614
|
+
|
|
615
|
+
if (Object.keys(contexts).length === 0 && parsed.some((part) => part.parts.factElements.length > 0)) {
|
|
616
|
+
throw new IxbrlError(`This file is one part of a document set: its contexts are in another file. ${DROP_THE_SET}`);
|
|
514
617
|
}
|
|
515
618
|
|
|
516
619
|
const facts = [];
|
|
517
620
|
const seen = new Map();
|
|
518
621
|
const factNamespaces = new Set();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
factNamespaces.add(namespace);
|
|
529
|
-
|
|
530
|
-
const contextRef = element.getAttribute("contextRef");
|
|
531
|
-
const context = contexts[contextRef];
|
|
532
|
-
if (!context) throw new IxbrlError(`The fact ${localName} refers to the context "${contextRef}", which the file does not define.`);
|
|
533
|
-
|
|
534
|
-
const numeric = kind === "ixbrl" ? isIx(element, "nonFraction") : element.hasAttribute("unitRef");
|
|
535
|
-
let value;
|
|
536
|
-
let unit = null;
|
|
537
|
-
let decimals;
|
|
538
|
-
if (numeric) {
|
|
539
|
-
const unitRef = element.getAttribute("unitRef");
|
|
540
|
-
unit = units[unitRef];
|
|
541
|
-
if (unit === undefined)
|
|
542
|
-
throw new IxbrlError(`The fact ${localName} refers to the unit "${unitRef}", which the file does not define.`);
|
|
543
|
-
if (unit !== "GBP" && unit !== "pure" && unit !== "shares" && !unit.startsWith("divide:")) {
|
|
544
|
-
throw new IxbrlError(`The fact ${localName} is stated in ${unit}; diya-gl reads pounds sterling (GBP), shares and pure numbers.`);
|
|
622
|
+
const leftOut = new Set();
|
|
623
|
+
let defaultTargetElements = 0;
|
|
624
|
+
const shared = { contexts, units, continuations };
|
|
625
|
+
for (const part of parsed) {
|
|
626
|
+
for (const element of part.parts.factElements) {
|
|
627
|
+
const target = kind === "ixbrl" ? element.getAttribute("target") : null;
|
|
628
|
+
if (target) {
|
|
629
|
+
leftOut.add(target);
|
|
630
|
+
continue;
|
|
545
631
|
}
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
value,
|
|
563
|
-
hidden: insideHidden(element, parts.hiddenElements),
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
const key = `${namespace}#${localName}|${contextKey(context)}|${unit ?? ""}`;
|
|
567
|
-
if (seen.has(key)) {
|
|
568
|
-
const first = seen.get(key);
|
|
569
|
-
if (first.value !== value) {
|
|
570
|
-
throw new IxbrlError(`The file states ${localName} twice for the same period with different values (${first.value} and ${value}).`);
|
|
632
|
+
const fact = readFactElement(element, part, shared);
|
|
633
|
+
if (!fact) continue;
|
|
634
|
+
defaultTargetElements += 1;
|
|
635
|
+
factNamespaces.add(fact.namespace);
|
|
636
|
+
|
|
637
|
+
const context = contexts[fact.contextRef];
|
|
638
|
+
const key = `${fact.namespace}#${fact.localName}|${contextKey(context)}|${fact.unit ?? ""}`;
|
|
639
|
+
if (seen.has(key)) {
|
|
640
|
+
const first = seen.get(key);
|
|
641
|
+
if (first.value !== fact.value) {
|
|
642
|
+
throw new IxbrlError(
|
|
643
|
+
`${many ? "The files state" : "The file states"} ${fact.localName} twice for the same period with different values (${first.value} and ${fact.value}).`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
first.hidden = first.hidden && fact.hidden;
|
|
647
|
+
continue;
|
|
571
648
|
}
|
|
572
|
-
|
|
573
|
-
|
|
649
|
+
seen.set(key, fact);
|
|
650
|
+
facts.push(fact);
|
|
574
651
|
}
|
|
575
|
-
|
|
576
|
-
|
|
652
|
+
}
|
|
653
|
+
if (defaultTargetElements === 0 && leftOut.size > 0) {
|
|
654
|
+
throw new IxbrlError(`The set has no facts for the default target; its facts are for the targets ${[...leftOut].join(", ")}.`);
|
|
577
655
|
}
|
|
578
656
|
|
|
579
|
-
const suite = suiteOf(
|
|
657
|
+
const suite = suiteOf(
|
|
658
|
+
factNamespaces,
|
|
659
|
+
parsed.flatMap((part) => part.parts.schemaRefs),
|
|
660
|
+
);
|
|
580
661
|
checkSuite(suite);
|
|
581
662
|
|
|
582
|
-
return {
|
|
663
|
+
return {
|
|
664
|
+
kind,
|
|
665
|
+
suite: { family: suite.family, date: suite.date, modules: [...suite.modules] },
|
|
666
|
+
facts,
|
|
667
|
+
contexts,
|
|
668
|
+
units,
|
|
669
|
+
leftOutTargets: [...leftOut],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Read an Inline XBRL document or an XBRL instance into facts.
|
|
675
|
+
* @param {Uint8Array|string} input - the document
|
|
676
|
+
* @returns {{kind: "ixbrl"|"xbrl", suite: {family: string|null, date: string|null, modules: string[]}, facts: Array<Object>, contexts: Object, units: Object, leftOutTargets: string[]}}
|
|
677
|
+
* @throws {IxbrlError} when the document cannot be read, with a message a customer reads
|
|
678
|
+
*/
|
|
679
|
+
export function readIxbrl(input) {
|
|
680
|
+
return readIxbrlSet([{ name: "", bytes: input }]);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Whether two reads of one report state the same entity, periods, numbers and
|
|
687
|
+
* dates. A Welsh package holds the report in two languages and Companies House
|
|
688
|
+
* rejects it when they differ.
|
|
689
|
+
* @param {Object} first - a read from readIxbrlSet
|
|
690
|
+
* @param {Object} second - a read from readIxbrlSet
|
|
691
|
+
* @param {string} firstLabel - what to call the first in the refusal
|
|
692
|
+
* @param {string} secondLabel
|
|
693
|
+
* @throws {IxbrlError} naming the first concept that differs
|
|
694
|
+
*/
|
|
695
|
+
export function checkSameReport(first, second, firstLabel, secondLabel) {
|
|
696
|
+
const entities = (read) => [...new Set(Object.values(read.contexts).map((context) => context.entity.identifier))].sort().join(",");
|
|
697
|
+
if (entities(first) !== entities(second)) {
|
|
698
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts are for different companies.`);
|
|
699
|
+
}
|
|
700
|
+
const periods = (read) =>
|
|
701
|
+
[
|
|
702
|
+
...new Set(
|
|
703
|
+
Object.values(read.contexts)
|
|
704
|
+
.filter((context) => !Object.keys(context.dimensions).length)
|
|
705
|
+
.map((context) => periodKey(context.period)),
|
|
706
|
+
),
|
|
707
|
+
]
|
|
708
|
+
.sort()
|
|
709
|
+
.join(",");
|
|
710
|
+
if (periods(first) !== periods(second)) {
|
|
711
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts are for different periods.`);
|
|
712
|
+
}
|
|
713
|
+
const figures = (read) =>
|
|
714
|
+
new Map(
|
|
715
|
+
read.facts
|
|
716
|
+
.filter((fact) => fact.unit !== null || (typeof fact.value === "string" && DATE_TEXT.test(fact.value)))
|
|
717
|
+
.map((fact) => [
|
|
718
|
+
`${fact.namespace}#${fact.localName}|${contextKey({ entity: fact.entity, period: fact.period, dimensions: fact.dimensions })}|${fact.unit ?? ""}`,
|
|
719
|
+
fact,
|
|
720
|
+
]),
|
|
721
|
+
);
|
|
722
|
+
const a = figures(first);
|
|
723
|
+
const b = figures(second);
|
|
724
|
+
for (const [key, fact] of a) {
|
|
725
|
+
if (!b.has(key) || b.get(key).value !== fact.value) {
|
|
726
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts differ at ${fact.localName}.`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
for (const [key, fact] of b) {
|
|
730
|
+
if (!a.has(key)) throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts differ at ${fact.localName}.`);
|
|
731
|
+
}
|
|
583
732
|
}
|