ixbrl-viewer 1.4.1__py3-none-any.whl → 1.4.86__py3-none-any.whl
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.
- iXBRLViewerPlugin/__init__.py +231 -127
- iXBRLViewerPlugin/_version.py +33 -3
- iXBRLViewerPlugin/constants.py +96 -2
- iXBRLViewerPlugin/featureConfig.py +8 -1
- iXBRLViewerPlugin/iXBRLViewer.py +356 -214
- iXBRLViewerPlugin/plugin.py +12 -0
- iXBRLViewerPlugin/ui.py +81 -50
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js.LICENSE.txt +12 -5
- iXBRLViewerPlugin/viewer/i18next-parser.config.js +1 -1
- iXBRLViewerPlugin/viewer/src/data/utr.json +1244 -0
- iXBRLViewerPlugin/viewer/src/html/fact-details.html +69 -38
- iXBRLViewerPlugin/viewer/src/html/footer-logo.html +4 -0
- iXBRLViewerPlugin/viewer/src/html/footnote-details.html +2 -2
- iXBRLViewerPlugin/viewer/src/html/inspector.html +352 -197
- iXBRLViewerPlugin/viewer/src/i18n/cy/balancetypes.json +1 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/scale.json +16 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/cy/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/scale.json +15 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/da/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/scale.json +15 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/de/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/datatypes.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/labelroles.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/scale.json +16 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +73 -23
- iXBRLViewerPlugin/viewer/src/i18n/es/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/es/datatypes.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/es/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/es/scale.json +16 -0
- iXBRLViewerPlugin/viewer/src/i18n/es/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/es/translation.json +87 -37
- iXBRLViewerPlugin/viewer/src/i18n/fr/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/scale.json +15 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/fr/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/scale.json +15 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/nl/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/balancetypes.json +4 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/currencies.json +13 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/datatypes.json +9 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/labelroles.json +24 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/referenceparts.json +10 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/scale.json +15 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/tooltips.json +17 -0
- iXBRLViewerPlugin/viewer/src/i18n/uk/translation.json +179 -0
- iXBRLViewerPlugin/viewer/src/icons/calculator.svg +13 -0
- iXBRLViewerPlugin/viewer/src/icons/circle-cross.svg +11 -0
- iXBRLViewerPlugin/viewer/src/icons/circle-tick.svg +11 -0
- iXBRLViewerPlugin/viewer/src/icons/dark-mode.svg +4 -0
- iXBRLViewerPlugin/viewer/src/icons/dimension.svg +1 -5
- iXBRLViewerPlugin/viewer/src/icons/member.svg +2 -5
- iXBRLViewerPlugin/viewer/src/icons/multi-tag.svg +10 -0
- iXBRLViewerPlugin/viewer/src/img/arelle-dark.svg +179 -0
- iXBRLViewerPlugin/viewer/src/img/inline-viewer-dark.svg +59 -0
- iXBRLViewerPlugin/viewer/src/js/accordian.js +5 -4
- iXBRLViewerPlugin/viewer/src/js/aspect.js +29 -10
- iXBRLViewerPlugin/viewer/src/js/aspect.test.js +40 -31
- iXBRLViewerPlugin/viewer/src/js/balance.js +14 -0
- iXBRLViewerPlugin/viewer/src/js/calculation.js +213 -0
- iXBRLViewerPlugin/viewer/src/js/calculation.test.js +306 -0
- iXBRLViewerPlugin/viewer/src/js/calculationInspector.js +187 -0
- iXBRLViewerPlugin/viewer/src/js/chart.js +26 -24
- iXBRLViewerPlugin/viewer/src/js/chart.test.js +10 -9
- iXBRLViewerPlugin/viewer/src/js/concept.js +37 -4
- iXBRLViewerPlugin/viewer/src/js/concept.test.js +30 -6
- iXBRLViewerPlugin/viewer/src/js/datatype.js +20 -0
- iXBRLViewerPlugin/viewer/src/js/datatype.test.js +62 -0
- iXBRLViewerPlugin/viewer/src/js/dialog.js +6 -4
- iXBRLViewerPlugin/viewer/src/js/docOrderIndex.js +7 -7
- iXBRLViewerPlugin/viewer/src/js/fact.js +156 -59
- iXBRLViewerPlugin/viewer/src/js/fact.test.js +160 -29
- iXBRLViewerPlugin/viewer/src/js/factset.js +64 -15
- iXBRLViewerPlugin/viewer/src/js/factset.test.js +102 -31
- iXBRLViewerPlugin/viewer/src/js/footnote.js +8 -2
- iXBRLViewerPlugin/viewer/src/js/index.js +11 -3
- iXBRLViewerPlugin/viewer/src/js/inspector.js +747 -221
- iXBRLViewerPlugin/viewer/src/js/inspector.test.js +143 -25
- iXBRLViewerPlugin/viewer/src/js/interval.js +70 -0
- iXBRLViewerPlugin/viewer/src/js/interval.test.js +153 -0
- iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.js +391 -262
- iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.test.js +134 -20
- iXBRLViewerPlugin/viewer/src/js/ixnode.js +1 -1
- iXBRLViewerPlugin/viewer/src/js/menu.js +25 -7
- iXBRLViewerPlugin/viewer/src/js/number-matcher.js +7 -3
- iXBRLViewerPlugin/viewer/src/js/number-matcher.test.js +4 -0
- iXBRLViewerPlugin/viewer/src/js/outline.js +34 -13
- iXBRLViewerPlugin/viewer/src/js/outline.test.js +97 -91
- iXBRLViewerPlugin/viewer/src/js/period.js +0 -1
- iXBRLViewerPlugin/viewer/src/js/report.js +260 -351
- iXBRLViewerPlugin/viewer/src/js/report.test.js +95 -27
- iXBRLViewerPlugin/viewer/src/js/reportset.js +264 -0
- iXBRLViewerPlugin/viewer/src/js/reportset.test.js +357 -0
- iXBRLViewerPlugin/viewer/src/js/search.js +72 -38
- iXBRLViewerPlugin/viewer/src/js/search.test.js +184 -84
- iXBRLViewerPlugin/viewer/src/js/summary.js +34 -8
- iXBRLViewerPlugin/viewer/src/js/summary.test.js +69 -25
- iXBRLViewerPlugin/viewer/src/js/tableExport.js +9 -9
- iXBRLViewerPlugin/viewer/src/js/taxonomynamer.js +34 -0
- iXBRLViewerPlugin/viewer/src/js/taxonomynamer.test.js +32 -0
- iXBRLViewerPlugin/viewer/src/js/test-utils.js +46 -0
- iXBRLViewerPlugin/viewer/src/js/theme.js +50 -0
- iXBRLViewerPlugin/viewer/src/js/unit.js +90 -32
- iXBRLViewerPlugin/viewer/src/js/unit.test.js +62 -25
- iXBRLViewerPlugin/viewer/src/js/util.js +94 -0
- iXBRLViewerPlugin/viewer/src/js/util.test.js +33 -1
- iXBRLViewerPlugin/viewer/src/js/utr.js +27 -0
- iXBRLViewerPlugin/viewer/src/js/viewer.js +205 -181
- iXBRLViewerPlugin/viewer/src/js/viewerOptions.js +0 -2
- iXBRLViewerPlugin/viewer/src/less/accordian.less +10 -6
- iXBRLViewerPlugin/viewer/src/less/block-list.less +16 -5
- iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +83 -0
- iXBRLViewerPlugin/viewer/src/less/chart.less +8 -5
- iXBRLViewerPlugin/viewer/src/less/colours-dark-mode.less +40 -0
- iXBRLViewerPlugin/viewer/src/less/colours.less +32 -20
- iXBRLViewerPlugin/viewer/src/less/common.less +3 -3
- iXBRLViewerPlugin/viewer/src/less/components.less +6 -4
- iXBRLViewerPlugin/viewer/src/less/core.less +2 -0
- iXBRLViewerPlugin/viewer/src/less/dialog.less +21 -14
- iXBRLViewerPlugin/viewer/src/less/form-controls.less +33 -11
- iXBRLViewerPlugin/viewer/src/less/inspector.less +1045 -726
- iXBRLViewerPlugin/viewer/src/less/loader.less +2 -2
- iXBRLViewerPlugin/viewer/src/less/menu.less +33 -15
- iXBRLViewerPlugin/viewer/src/less/summary.less +16 -6
- iXBRLViewerPlugin/viewer/src/less/tabs.less +9 -9
- iXBRLViewerPlugin/viewer/src/less/text-block-viewer.less +2 -0
- iXBRLViewerPlugin/viewer/src/less/text-mixins.less +2 -1
- iXBRLViewerPlugin/viewer/src/less/validation-report.less +2 -3
- iXBRLViewerPlugin/viewer/src/less/viewer.less +105 -74
- iXBRLViewerPlugin/viewer/webpack.common.js +19 -9
- iXBRLViewerPlugin/xhtmlserialize.py +59 -45
- {ixbrl_viewer-1.4.1.dist-info → ixbrl_viewer-1.4.86.dist-info}/METADATA +181 -50
- ixbrl_viewer-1.4.86.dist-info/RECORD +217 -0
- {ixbrl_viewer-1.4.1.dist-info → ixbrl_viewer-1.4.86.dist-info}/WHEEL +1 -1
- ixbrl_viewer-1.4.1.dist-info/LICENSE → ixbrl_viewer-1.4.86.dist-info/licenses/LICENSE.md +8 -14
- {ixbrl_viewer-1.4.1.dist-info → ixbrl_viewer-1.4.86.dist-info}/top_level.txt +0 -1
- iXBRLViewerPlugin/viewer/src/js/calculations.js +0 -111
- iXBRLViewerPlugin/viewer/src/js/interact.min.js +0 -6
- ixbrl_viewer-1.4.1.dist-info/RECORD +0 -155
- tests/__init__.py +0 -0
- tests/puppeteer/framework/core_elements.js +0 -117
- tests/puppeteer/framework/page_objects/doc_frame.js +0 -105
- tests/puppeteer/framework/page_objects/fact_details_panel.js +0 -80
- tests/puppeteer/framework/page_objects/search_panel.js +0 -76
- tests/puppeteer/framework/page_objects/toolbar.js +0 -18
- tests/puppeteer/framework/utils.js +0 -3
- tests/puppeteer/framework/viewer_page.js +0 -103
- tests/puppeteer/puppeteer_test_run_via_intellij.jpg +0 -0
- tests/puppeteer/test_filings/filing_documents_smoke_test.zip +0 -0
- tests/puppeteer/test_filings/highlights.zip +0 -0
- tests/puppeteer/tests/fact_properties.test.js +0 -78
- tests/puppeteer/tests/highlight.test.js +0 -186
- tests/puppeteer/tests/search.test.js +0 -86
- tests/puppeteer/tools/generate.sh +0 -15
- tests/unit_tests/__init__.py +0 -0
- tests/unit_tests/iXBRLViewerPlugin/__init__.py +0 -0
- tests/unit_tests/iXBRLViewerPlugin/mock_arelle.py +0 -39
- tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +0 -641
- tests/unit_tests/iXBRLViewerPlugin/test_xhtmlserialize.py +0 -310
- {ixbrl_viewer-1.4.1.dist-info → ixbrl_viewer-1.4.86.dist-info}/entry_points.txt +0 -0
- {ixbrl_viewer-1.4.1.dist-info → ixbrl_viewer-1.4.86.dist-info/licenses}/NOTICE +0 -0
iXBRLViewerPlugin/iXBRLViewer.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# See COPYRIGHT.md for copyright information
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
@@ -12,21 +13,32 @@ import urllib.parse
|
|
|
12
13
|
import zipfile
|
|
13
14
|
from collections import defaultdict
|
|
14
15
|
from copy import deepcopy
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from arelle import
|
|
19
|
-
from arelle.
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Literal, cast
|
|
18
|
+
|
|
19
|
+
from arelle import XbrlConst
|
|
20
|
+
from arelle.Cntlr import Cntlr
|
|
21
|
+
from arelle.ModelDocument import ModelDocument, Type
|
|
22
|
+
from arelle.ModelDtsObject import ModelConcept
|
|
23
|
+
from arelle.ModelInstanceObject import ModelInlineFact, ModelUnit
|
|
20
24
|
from arelle.ModelRelationshipSet import ModelRelationshipSet
|
|
21
|
-
from arelle.ModelValue import
|
|
25
|
+
from arelle.ModelValue import INVALIDixVALUE, QName
|
|
26
|
+
from arelle.ModelXbrl import ModelXbrl
|
|
22
27
|
from arelle.UrlUtil import isHttpUrl
|
|
23
28
|
from arelle.ValidateXbrlCalcs import inferredDecimals
|
|
24
29
|
from lxml import etree
|
|
25
30
|
|
|
26
|
-
from .constants import
|
|
31
|
+
from .constants import (
|
|
32
|
+
DEFAULT_JS_FILENAME,
|
|
33
|
+
DEFAULT_OUTPUT_NAME,
|
|
34
|
+
ERROR_MESSAGE_CODE,
|
|
35
|
+
FEATURE_CONFIGS,
|
|
36
|
+
INFO_MESSAGE_CODE,
|
|
37
|
+
MANDATORY_FACTS,
|
|
38
|
+
)
|
|
27
39
|
from .xhtmlserialize import XHTMLSerializer
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
REPORT_TYPE_EXTENSIONS = ('.xbrl', '.xhtml', '.html', '.htm', '.json')
|
|
30
42
|
UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE = 'unrecognizedLinkbase'
|
|
31
43
|
LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE = {
|
|
32
44
|
XbrlConst.qnLinkCalculationLink: 'calcLinkbase',
|
|
@@ -45,11 +57,11 @@ class NamespaceMap:
|
|
|
45
57
|
required.
|
|
46
58
|
"""
|
|
47
59
|
|
|
48
|
-
def __init__(self):
|
|
49
|
-
self.nsmap =
|
|
50
|
-
self.prefixmap =
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self.nsmap: dict[str, str] = {}
|
|
62
|
+
self.prefixmap: dict[str, str] = {}
|
|
51
63
|
|
|
52
|
-
def getPrefix(self, ns, preferredPrefix = None):
|
|
64
|
+
def getPrefix(self, ns: str, preferredPrefix: str | None = None) -> str:
|
|
53
65
|
"""
|
|
54
66
|
Get the prefix for the specified namespace.
|
|
55
67
|
|
|
@@ -65,58 +77,85 @@ class NamespaceMap:
|
|
|
65
77
|
else:
|
|
66
78
|
p = preferredPrefix if preferredPrefix else "ns"
|
|
67
79
|
n = 0
|
|
68
|
-
while "
|
|
80
|
+
while f"{p}{n}" in self.prefixmap:
|
|
69
81
|
n += 1
|
|
70
82
|
|
|
71
|
-
prefix = "
|
|
83
|
+
prefix = f"{p}{n}"
|
|
72
84
|
|
|
73
85
|
self.prefixmap[prefix] = ns
|
|
74
86
|
self.nsmap[ns] = prefix
|
|
75
87
|
return prefix
|
|
76
88
|
|
|
77
|
-
def qname(self, qname):
|
|
78
|
-
|
|
89
|
+
def qname(self, qname: QName) -> str:
|
|
90
|
+
if qname.namespaceURI is None:
|
|
91
|
+
return qname.localName
|
|
92
|
+
return f"{self.getPrefix(qname.namespaceURI, qname.prefix)}:{qname.localName}"
|
|
79
93
|
|
|
80
94
|
class IXBRLViewerBuilderError(Exception):
|
|
81
95
|
pass
|
|
82
96
|
|
|
97
|
+
def isInlineDoc(doc: ModelDocument | None) -> bool:
|
|
98
|
+
return doc is not None and doc.type in {Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET}
|
|
99
|
+
|
|
83
100
|
class IXBRLViewerBuilder:
|
|
84
101
|
|
|
85
|
-
def __init__(
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
cntlr: Cntlr,
|
|
105
|
+
basenameSuffix: str = "",
|
|
106
|
+
useStubViewer: bool = False,
|
|
107
|
+
features: dict[str, Any] | None = None,
|
|
108
|
+
):
|
|
109
|
+
if features is None:
|
|
110
|
+
features = {}
|
|
111
|
+
featureNames = {c.key for c in FEATURE_CONFIGS}
|
|
112
|
+
for featureName in features:
|
|
113
|
+
assert featureName in featureNames, \
|
|
114
|
+
f'Given feature name `{featureName}` does not match any defined features: {featureNames}'
|
|
115
|
+
self.reportZip: str | None = None
|
|
86
116
|
self.nsmap = NamespaceMap()
|
|
87
117
|
self.roleMap = NamespaceMap()
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
"languages": {},
|
|
92
|
-
"facts": {},
|
|
93
|
-
"features": [],
|
|
118
|
+
self.taxonomyData: dict[str, Any] = {
|
|
119
|
+
"sourceReports": [],
|
|
120
|
+
"features": features,
|
|
94
121
|
}
|
|
95
|
-
self.footnoteRelationshipSet = ModelRelationshipSet(dts, "XBRL-footnotes")
|
|
96
122
|
self.basenameSuffix = basenameSuffix
|
|
123
|
+
self.currentTargetReport: dict[str, Any] | None = None
|
|
124
|
+
self.useStubViewer = useStubViewer
|
|
125
|
+
self.cntlr = cntlr
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.
|
|
127
|
+
self.idGen = 0
|
|
128
|
+
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
129
|
+
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
130
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
131
|
+
self.roleMap.getPrefix(XbrlConst.summationItem11, "calc11")
|
|
132
|
+
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
133
|
+
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
134
|
+
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
135
|
+
|
|
136
|
+
self.sourceReportsByFiles: dict[frozenset[str], dict[str, Any]] = {}
|
|
137
|
+
self.iv = iXBRLViewer(cntlr)
|
|
138
|
+
if self.useStubViewer:
|
|
139
|
+
self.iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
105
140
|
|
|
106
|
-
|
|
141
|
+
self.fromSingleZIP: bool | None = None
|
|
142
|
+
self.reportCount = 0
|
|
143
|
+
self.assets: list[str] = []
|
|
144
|
+
|
|
145
|
+
def outputFilename(self, filename: str) -> str:
|
|
107
146
|
(base, ext) = os.path.splitext(filename)
|
|
108
147
|
return base + self.basenameSuffix + ext
|
|
109
148
|
|
|
110
|
-
def lineWrap(self, s, n = 80):
|
|
149
|
+
def lineWrap(self, s: str, n: int = 80) -> str:
|
|
111
150
|
return "\n".join([s[i:i+n] for i in range(0, len(s), n)])
|
|
112
151
|
|
|
113
|
-
def dateFormat(self, d):
|
|
152
|
+
def dateFormat(self, d: str) -> str:
|
|
114
153
|
"""
|
|
115
154
|
Strip the time component from an ISO date if it's zero
|
|
116
155
|
"""
|
|
117
156
|
return re.sub("T00:00:00$", "", d)
|
|
118
157
|
|
|
119
|
-
def escapeJSONForScriptTag(self, s):
|
|
158
|
+
def escapeJSONForScriptTag(self, s: str) -> str:
|
|
120
159
|
"""
|
|
121
160
|
JSON encodes XML special characters XML and HTML apply difference escaping rules to content
|
|
122
161
|
within script tags and we need our output to be valid XML, but treated as HTML by browsers.
|
|
@@ -130,52 +169,38 @@ class IXBRLViewerBuilder:
|
|
|
130
169
|
"""
|
|
131
170
|
return s.replace("<","\\u003C").replace(">","\\u003E").replace("&","\\u0026")
|
|
132
171
|
|
|
133
|
-
def
|
|
134
|
-
code = re.sub("-.*","",langCode)
|
|
135
|
-
try:
|
|
136
|
-
language = pycountry.languages.lookup(code)
|
|
137
|
-
match = re.match(r'^[^-]+-(.*)$',langCode)
|
|
138
|
-
name = language.name
|
|
139
|
-
if match is not None:
|
|
140
|
-
name = "%s (%s)" % (name, match.group(1).upper())
|
|
141
|
-
except LookupError:
|
|
142
|
-
name = langCode
|
|
143
|
-
|
|
144
|
-
return name
|
|
145
|
-
|
|
146
|
-
def addLanguage(self, langCode):
|
|
147
|
-
if langCode not in self.taxonomyData["languages"]:
|
|
148
|
-
self.taxonomyData["languages"][langCode] = self.makeLanguageName(langCode)
|
|
149
|
-
|
|
150
|
-
def addELR(self, elr):
|
|
172
|
+
def addRoleDefinition(self, report: ModelXbrl, elr: str) -> None:
|
|
151
173
|
prefix = self.roleMap.getPrefix(elr)
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
assert self.currentTargetReport is not None, "Current target report must be set to add role definition"
|
|
175
|
+
if self.currentTargetReport.setdefault("roleDefs",{}).get(prefix, None) is None:
|
|
176
|
+
rts = report.roleTypes.get(elr, [])
|
|
154
177
|
label = next((rt.definition for rt in rts if rt.definition is not None), None)
|
|
155
178
|
if label is not None:
|
|
156
|
-
self.
|
|
179
|
+
self.currentTargetReport["roleDefs"].setdefault(prefix,{})["en"] = label
|
|
157
180
|
|
|
158
|
-
def addConcept(self, concept, dimensionType = None):
|
|
181
|
+
def addConcept(self, report: ModelXbrl, concept: ModelConcept | None, dimensionType: str | None = None) -> None:
|
|
159
182
|
if concept is None:
|
|
160
183
|
return
|
|
161
|
-
labelsRelationshipSet =
|
|
184
|
+
labelsRelationshipSet = report.relationshipSet(XbrlConst.conceptLabel)
|
|
162
185
|
labels = labelsRelationshipSet.fromModelObject(concept)
|
|
163
186
|
conceptName = self.nsmap.qname(concept.qname)
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
assert self.currentTargetReport is not None, "Current target report must be set to add concept"
|
|
188
|
+
if conceptName not in self.currentTargetReport["concepts"]:
|
|
189
|
+
conceptData: dict[str, Any] = {
|
|
166
190
|
"labels": { }
|
|
167
191
|
}
|
|
168
192
|
for lr in labels:
|
|
169
193
|
l = lr.toModelObject
|
|
170
194
|
conceptData["labels"].setdefault(self.roleMap.getPrefix(l.role),{})[l.xmlLang.lower()] = l.text;
|
|
171
|
-
self.
|
|
195
|
+
self.addRoleDefinition(report, l.role)
|
|
172
196
|
|
|
173
197
|
refData = []
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
if concept.modelXbrl is not None:
|
|
199
|
+
for _refRel in concept.modelXbrl.relationshipSet(XbrlConst.conceptReference).fromModelObject(concept):
|
|
200
|
+
ref = []
|
|
201
|
+
for _refPart in _refRel.toModelObject.iterchildren():
|
|
202
|
+
ref.append([_refPart.localName, _refPart.stringValue.strip()])
|
|
203
|
+
refData.append(ref)
|
|
179
204
|
|
|
180
205
|
if len(refData) > 0:
|
|
181
206
|
conceptData['r'] = refData
|
|
@@ -186,32 +211,45 @@ class IXBRLViewerBuilder:
|
|
|
186
211
|
if concept.isEnumeration:
|
|
187
212
|
conceptData["e"] = True
|
|
188
213
|
|
|
189
|
-
if concept.
|
|
214
|
+
if concept.isTextBlock:
|
|
190
215
|
conceptData['t'] = True
|
|
191
216
|
|
|
217
|
+
if concept.balance is not None:
|
|
218
|
+
conceptData['b'] = concept.balance
|
|
219
|
+
|
|
220
|
+
if concept.type is not None:
|
|
221
|
+
conceptData['dt'] = self.nsmap.qname(concept.type.qname)
|
|
222
|
+
|
|
192
223
|
if concept.isTypedDimension:
|
|
193
224
|
typedDomainElement = concept.typedDomainElement
|
|
194
225
|
if typedDomainElement is not None:
|
|
195
226
|
typedDomainName = self.nsmap.qname(typedDomainElement.qname)
|
|
196
227
|
conceptData['td'] = typedDomainName
|
|
197
|
-
self.addConcept(typedDomainElement)
|
|
228
|
+
self.addConcept(report, typedDomainElement)
|
|
198
229
|
|
|
199
|
-
self.
|
|
230
|
+
self.currentTargetReport["concepts"][conceptName] = conceptData
|
|
200
231
|
|
|
201
|
-
def treeWalk(self, rels, item, indent = 0):
|
|
232
|
+
def treeWalk(self, rels: ModelRelationshipSet, item: Any, indent: int = 0) -> None:
|
|
202
233
|
for r in rels.fromModelObject(item):
|
|
203
234
|
if r.toModelObject is not None:
|
|
204
235
|
self.treeWalk(rels, r.toModelObject, indent + 1)
|
|
205
236
|
|
|
206
|
-
def getRelationships(self):
|
|
207
|
-
rels = {}
|
|
237
|
+
def getRelationships(self, report: ModelXbrl) -> dict[str, dict[str, dict[str, list[dict[str, str]]]]]:
|
|
238
|
+
rels: dict[str, dict[str, dict[str, list[dict[str, str]]]]] = {}
|
|
208
239
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
arcroles = {
|
|
241
|
+
XbrlConst.summationItem,
|
|
242
|
+
XbrlConst.summationItem11,
|
|
243
|
+
WIDER_NARROWER_ARCROLE,
|
|
244
|
+
XbrlConst.parentChild,
|
|
245
|
+
XbrlConst.dimensionDefault,
|
|
246
|
+
}
|
|
247
|
+
for baseSetKey in report.baseSets:
|
|
248
|
+
arcrole, ELR, _linkqname, _arcqname = baseSetKey
|
|
249
|
+
if ELR is not None and arcrole in arcroles:
|
|
250
|
+
self.addRoleDefinition(report, ELR)
|
|
251
|
+
rr: dict[str, list[dict[str, str]]] = {}
|
|
252
|
+
relSet = report.relationshipSet(arcrole, ELR)
|
|
215
253
|
for r in relSet.modelRelationships:
|
|
216
254
|
if r.fromModelObject is not None and r.toModelObject is not None:
|
|
217
255
|
fromKey = self.nsmap.qname(r.fromModelObject.qname)
|
|
@@ -221,21 +259,19 @@ class IXBRLViewerBuilder:
|
|
|
221
259
|
if r.weight is not None:
|
|
222
260
|
rel['w'] = r.weight
|
|
223
261
|
rr.setdefault(fromKey, []).append(rel)
|
|
224
|
-
self.addConcept(r.toModelObject)
|
|
225
|
-
self.addConcept(r.fromModelObject)
|
|
262
|
+
self.addConcept(report, r.toModelObject)
|
|
263
|
+
self.addConcept(report, r.fromModelObject)
|
|
226
264
|
|
|
227
|
-
rels.setdefault(self.roleMap.getPrefix(arcrole),{})[self.roleMap.getPrefix(ELR)] = rr
|
|
265
|
+
rels.setdefault(self.roleMap.getPrefix(arcrole), {})[self.roleMap.getPrefix(ELR)] = rr
|
|
228
266
|
return rels
|
|
229
267
|
|
|
230
|
-
def validationErrors(self):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
logHandler = dts.modelManager.cntlr.logHandler
|
|
234
|
-
if getattr(logHandler, "logRecordBuffer") is None:
|
|
268
|
+
def validationErrors(self) -> list[dict[str, str]]:
|
|
269
|
+
logHandler = self.cntlr.logHandler
|
|
270
|
+
if getattr(logHandler, "logRecordBuffer", None) is None:
|
|
235
271
|
raise IXBRLViewerBuilderError("Logging is not configured to use a buffer. Unable to retrieve validation messages")
|
|
236
272
|
|
|
237
|
-
errors = []
|
|
238
|
-
for logRec in getattr(logHandler, "logRecordBuffer"):
|
|
273
|
+
errors: list[dict[str, str]] = []
|
|
274
|
+
for logRec in getattr(logHandler, "logRecordBuffer", []):
|
|
239
275
|
if logRec.levelno > logging.INFO:
|
|
240
276
|
errors.append({
|
|
241
277
|
"sev": logRec.levelname.title().upper(),
|
|
@@ -245,20 +281,23 @@ class IXBRLViewerBuilder:
|
|
|
245
281
|
|
|
246
282
|
return errors
|
|
247
283
|
|
|
248
|
-
def addFact(self, f):
|
|
284
|
+
def addFact(self, report: ModelXbrl, f: ModelInlineFact) -> None:
|
|
249
285
|
if f.id is None:
|
|
250
|
-
f.set("id","ixv
|
|
286
|
+
f.set("id", f"ixv-{self.idGen}")
|
|
251
287
|
|
|
252
288
|
self.idGen += 1
|
|
253
289
|
conceptName = self.nsmap.qname(f.qname)
|
|
290
|
+
factList = MANDATORY_FACTS.get(self.taxonomyData["features"].get("mandatory_facts"), [])
|
|
291
|
+
isMandatory = f.qname.localName in factList
|
|
254
292
|
scheme, ident = f.context.entityIdentifier
|
|
255
293
|
|
|
256
294
|
aspects = {
|
|
257
295
|
"c": conceptName,
|
|
258
296
|
"e": self.nsmap.qname(QName(self.nsmap.getPrefix(scheme,"e"), scheme, ident)),
|
|
297
|
+
"m": isMandatory
|
|
259
298
|
}
|
|
260
299
|
|
|
261
|
-
factData = {
|
|
300
|
+
factData: dict[str, Any] = {
|
|
262
301
|
"a": aspects,
|
|
263
302
|
}
|
|
264
303
|
|
|
@@ -266,11 +305,15 @@ class IXBRLViewerBuilder:
|
|
|
266
305
|
factData["v"] = None
|
|
267
306
|
elif f.concept is not None and f.concept.isEnumeration:
|
|
268
307
|
qnEnums = f.xValue
|
|
269
|
-
if
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
308
|
+
if qnEnums is None:
|
|
309
|
+
factData["v"] = f.value
|
|
310
|
+
factData["err"] = 'INVALID_IX_VALUE'
|
|
311
|
+
else:
|
|
312
|
+
if not isinstance(qnEnums, list):
|
|
313
|
+
qnEnums = (qnEnums,)
|
|
314
|
+
factData["v"] = " ".join(self.nsmap.qname(qn) for qn in qnEnums)
|
|
315
|
+
for qn in qnEnums:
|
|
316
|
+
self.addConcept(report, report.qnameConcepts.get(qn))
|
|
274
317
|
else:
|
|
275
318
|
factData["v"] = f.value
|
|
276
319
|
if f.value == INVALIDixVALUE:
|
|
@@ -292,24 +335,21 @@ class IXBRLViewerBuilder:
|
|
|
292
335
|
if d != float("INF") and not math.isnan(d):
|
|
293
336
|
factData["d"] = d
|
|
294
337
|
|
|
295
|
-
for
|
|
338
|
+
for v in f.context.qnameDims.values():
|
|
296
339
|
if v.memberQname is not None:
|
|
297
340
|
aspects[self.nsmap.qname(v.dimensionQname)] = self.nsmap.qname(v.memberQname)
|
|
298
|
-
self.addConcept(v.member)
|
|
299
|
-
self.addConcept(v.dimension, dimensionType = "e")
|
|
341
|
+
self.addConcept(report, v.member)
|
|
342
|
+
self.addConcept(report, v.dimension, dimensionType = "e")
|
|
300
343
|
elif v.typedMember is not None:
|
|
301
344
|
aspects[self.nsmap.qname(v.dimensionQname)] = v.typedMember.text
|
|
302
|
-
self.addConcept(v.dimension, dimensionType = "t")
|
|
345
|
+
self.addConcept(report, v.dimension, dimensionType = "t")
|
|
303
346
|
|
|
304
347
|
if f.context.isForeverPeriod:
|
|
305
348
|
aspects["p"] = "f"
|
|
306
349
|
elif f.context.isInstantPeriod and f.context.instantDatetime is not None:
|
|
307
350
|
aspects["p"] = self.dateFormat(f.context.instantDatetime.isoformat())
|
|
308
351
|
elif f.context.isStartEndPeriod and f.context.startDatetime is not None and f.context.endDatetime is not None:
|
|
309
|
-
aspects["p"] = "
|
|
310
|
-
self.dateFormat(f.context.startDatetime.isoformat()),
|
|
311
|
-
self.dateFormat(f.context.endDatetime.isoformat())
|
|
312
|
-
)
|
|
352
|
+
aspects["p"] = f"{self.dateFormat(f.context.startDatetime.isoformat())}/{self.dateFormat(f.context.endDatetime.isoformat())}"
|
|
313
353
|
|
|
314
354
|
frels = self.footnoteRelationshipSet.fromModelObject(f)
|
|
315
355
|
if frels:
|
|
@@ -317,10 +357,11 @@ class IXBRLViewerBuilder:
|
|
|
317
357
|
if frel.toModelObject is not None:
|
|
318
358
|
factData.setdefault("fn", []).append(frel.toModelObject.id)
|
|
319
359
|
|
|
320
|
-
self.
|
|
321
|
-
self.
|
|
360
|
+
assert self.currentTargetReport is not None, "Current target report must be set to add fact"
|
|
361
|
+
self.currentTargetReport["facts"][f.id] = factData
|
|
362
|
+
self.addConcept(report, f.concept)
|
|
322
363
|
|
|
323
|
-
def oimUnitString(self, unit):
|
|
364
|
+
def oimUnitString(self, unit: ModelUnit) -> str:
|
|
324
365
|
"""
|
|
325
366
|
Returns an OIM-format string representation of the given ModelUnit.
|
|
326
367
|
See https://www.xbrl.org/Specification/oim-common/REC-2021-10-13/oim-common-REC-2021-10-13.html#term-unit-string-representation
|
|
@@ -333,22 +374,22 @@ class IXBRLViewerBuilder:
|
|
|
333
374
|
denominatorsString = '*'.join(self.nsmap.qname(x) for x in sorted(denominators))
|
|
334
375
|
if len(denominators) > 1:
|
|
335
376
|
if len(numerators) > 1:
|
|
336
|
-
return "({})/({})"
|
|
337
|
-
return "{}/({})"
|
|
377
|
+
return f"({numeratorsString})/({denominatorsString})"
|
|
378
|
+
return f"{numeratorsString}/({denominatorsString})"
|
|
338
379
|
else:
|
|
339
380
|
if len(numerators) > 1:
|
|
340
|
-
return "({})/{}"
|
|
341
|
-
return "{}/{}"
|
|
381
|
+
return f"({numeratorsString})/{denominatorsString}"
|
|
382
|
+
return f"{numeratorsString}/{denominatorsString}"
|
|
342
383
|
return numeratorsString
|
|
343
384
|
|
|
344
|
-
def
|
|
385
|
+
def addViewerData(self, viewerFile: 'iXBRLViewerFile', scriptUrl: str) -> bool:
|
|
345
386
|
taxonomyDataJSON = self.escapeJSONForScriptTag(json.dumps(self.taxonomyData, indent=1, allow_nan=False))
|
|
346
387
|
|
|
347
|
-
for child in xmlDocument.getroot():
|
|
388
|
+
for child in viewerFile.xmlDocument.getroot():
|
|
348
389
|
if child.tag == '{http://www.w3.org/1999/xhtml}body':
|
|
349
390
|
for body_child in child:
|
|
350
|
-
if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
|
|
351
|
-
self.
|
|
391
|
+
if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type', '') == 'application/x.ixbrl-viewer+json':
|
|
392
|
+
self.cntlr.addToLog("File already contains iXBRL viewer", messageCode="error")
|
|
352
393
|
return False
|
|
353
394
|
|
|
354
395
|
child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
|
|
@@ -372,82 +413,85 @@ class IXBRLViewerBuilder:
|
|
|
372
413
|
return True
|
|
373
414
|
return False
|
|
374
415
|
|
|
375
|
-
def getStubDocument(self):
|
|
416
|
+
def getStubDocument(self) -> etree._ElementTree[etree._Element]:
|
|
376
417
|
with open(os.path.join(os.path.dirname(__file__),"stubviewer.html")) as fin:
|
|
377
418
|
return etree.parse(fin)
|
|
378
419
|
|
|
379
|
-
def
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
:return: An iXBRLViewer instance that is ready to be saved.
|
|
386
|
-
"""
|
|
387
|
-
dts = self.dts
|
|
388
|
-
iv = iXBRLViewer(dts)
|
|
389
|
-
self.idGen = 0
|
|
390
|
-
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
391
|
-
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
392
|
-
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
393
|
-
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
394
|
-
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
395
|
-
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
396
|
-
|
|
397
|
-
docSetFiles = None
|
|
398
|
-
|
|
399
|
-
for f in dts.facts:
|
|
400
|
-
self.addFact(f)
|
|
401
|
-
|
|
402
|
-
self.taxonomyData["prefixes"] = self.nsmap.prefixmap
|
|
403
|
-
self.taxonomyData["roles"] = self.roleMap.prefixmap
|
|
404
|
-
self.taxonomyData["rels"] = self.getRelationships()
|
|
420
|
+
def newTargetReport(self, target: str | None) -> dict[str, Any]:
|
|
421
|
+
return {
|
|
422
|
+
"concepts": {},
|
|
423
|
+
"facts": {},
|
|
424
|
+
"target": target,
|
|
425
|
+
}
|
|
405
426
|
|
|
406
|
-
|
|
407
|
-
|
|
427
|
+
def addSourceReport(self) -> dict[str, list[Any]]:
|
|
428
|
+
sourceReport: dict[str, list[Any]] = {
|
|
429
|
+
"targetReports": []
|
|
430
|
+
}
|
|
431
|
+
self.taxonomyData["sourceReports"].append(sourceReport)
|
|
432
|
+
return sourceReport
|
|
433
|
+
|
|
434
|
+
def processModel(self, report: ModelXbrl) -> None:
|
|
435
|
+
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes") # type: ignore[no-untyped-call]
|
|
436
|
+
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
437
|
+
softwareCredits = set()
|
|
438
|
+
for document in report.urlDocs.values():
|
|
439
|
+
if isInlineDoc(document):
|
|
440
|
+
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
441
|
+
softwareCredits.update(matches)
|
|
442
|
+
if softwareCredits:
|
|
443
|
+
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
444
|
+
for f in report.facts:
|
|
445
|
+
if f.isTuple:
|
|
446
|
+
for nestedTupleFact in f.ixIter(): # type: ignore[attr-defined]
|
|
447
|
+
self.addFact(report, nestedTupleFact)
|
|
448
|
+
else:
|
|
449
|
+
self.addFact(report, cast(ModelInlineFact, f))
|
|
450
|
+
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
408
451
|
|
|
409
|
-
|
|
410
|
-
|
|
452
|
+
docSetFiles = None
|
|
453
|
+
self.reportCount += 1
|
|
454
|
+
report.info(
|
|
455
|
+
INFO_MESSAGE_CODE,
|
|
456
|
+
f"Creating iXBRL viewer ({self.reportCount}) [{self.currentTargetReport['target']}]",
|
|
457
|
+
)
|
|
458
|
+
assert report.modelDocument is not None
|
|
459
|
+
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
411
460
|
# Sort by object index to preserve order in which files were specified.
|
|
412
461
|
xmlDocsByFilename = {
|
|
413
|
-
os.path.basename(self.outputFilename(doc.filepath)):
|
|
414
|
-
for doc in sorted(
|
|
462
|
+
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
463
|
+
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
464
|
+
if doc.type == Type.INLINEXBRL
|
|
415
465
|
}
|
|
416
466
|
docSetFiles = list(xmlDocsByFilename.keys())
|
|
417
467
|
|
|
418
|
-
if useStubViewer:
|
|
419
|
-
xmlDocument = self.getStubDocument()
|
|
420
|
-
iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, xmlDocument))
|
|
421
|
-
else:
|
|
422
|
-
xmlDocument = next(iter(xmlDocsByFilename.values()))
|
|
423
|
-
|
|
424
468
|
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
425
|
-
iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
469
|
+
self.iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
426
470
|
|
|
427
|
-
elif useStubViewer:
|
|
428
|
-
|
|
429
|
-
filename = self.outputFilename(os.path.basename(dts.modelDocument.filepath))
|
|
471
|
+
elif self.useStubViewer:
|
|
472
|
+
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
430
473
|
docSetFiles = [ filename ]
|
|
431
|
-
iv.addFile(iXBRLViewerFile(
|
|
432
|
-
iv.addFile(iXBRLViewerFile(filename, dts.modelDocument.xmlDocument))
|
|
474
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
433
475
|
|
|
434
476
|
else:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
477
|
+
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
478
|
+
docSetFiles = [ srcFilename ]
|
|
479
|
+
filename = srcFilename
|
|
480
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
481
|
+
docSetKey = frozenset(docSetFiles)
|
|
482
|
+
sourceReport = self.sourceReportsByFiles.get(docSetKey)
|
|
483
|
+
if sourceReport is None:
|
|
484
|
+
sourceReport = self.addSourceReport()
|
|
485
|
+
self.sourceReportsByFiles[docSetKey] = sourceReport
|
|
486
|
+
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
487
|
+
|
|
488
|
+
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
445
489
|
|
|
446
490
|
localDocs = defaultdict(set)
|
|
447
|
-
for path, doc in
|
|
448
|
-
if isHttpUrl(path):
|
|
491
|
+
for path, doc in report.urlDocs.items():
|
|
492
|
+
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
449
493
|
continue
|
|
450
|
-
if doc.type
|
|
494
|
+
if doc.type == Type.INLINEXBRL:
|
|
451
495
|
localDocs[doc.basename].add('inline')
|
|
452
496
|
elif doc.type == Type.SCHEMA:
|
|
453
497
|
localDocs[doc.basename].add('schema')
|
|
@@ -460,40 +504,107 @@ class IXBRLViewerBuilder:
|
|
|
460
504
|
linkbaseIdentifed = True
|
|
461
505
|
if not linkbaseIdentifed:
|
|
462
506
|
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
463
|
-
self.
|
|
507
|
+
self.currentTargetReport["localDocs"] = {
|
|
464
508
|
localDoc: sorted(docTypes)
|
|
465
509
|
for localDoc, docTypes in localDocs.items()
|
|
466
510
|
}
|
|
467
511
|
|
|
468
|
-
|
|
469
|
-
|
|
512
|
+
# If we only process a single ZIP, add a download link to it as the
|
|
513
|
+
# "filing documents" on the viewer menu.
|
|
514
|
+
if self.fromSingleZIP is None:
|
|
515
|
+
self.fromSingleZIP = report.modelDocument.filepath.endswith(".zip")
|
|
516
|
+
if self.fromSingleZIP:
|
|
517
|
+
self.filingDocZipPath = os.path.dirname(report.modelDocument.filepath)
|
|
518
|
+
else:
|
|
519
|
+
self.fromSingleZIP = False
|
|
520
|
+
if report.fileSource.isArchive and isinstance(report.fileSource.fs, zipfile.ZipFile):
|
|
521
|
+
filelist = report.fileSource.fs.filelist
|
|
522
|
+
for file in filelist:
|
|
523
|
+
directory, asset = os.path.split(file.filename)
|
|
524
|
+
if "reports" in directory and asset != '' and not asset.lower().endswith(REPORT_TYPE_EXTENSIONS):
|
|
525
|
+
self.assets.append(file.filename)
|
|
526
|
+
if self.assets:
|
|
527
|
+
self.reportZip = report.fileSource.fs.filename
|
|
528
|
+
|
|
529
|
+
def createViewer(
|
|
530
|
+
self,
|
|
531
|
+
scriptUrl: str = DEFAULT_JS_FILENAME,
|
|
532
|
+
showValidations: bool = True,
|
|
533
|
+
packageDownloadURL: str | None = None,
|
|
534
|
+
) -> 'iXBRLViewer' | None:
|
|
535
|
+
"""
|
|
536
|
+
Create an iXBRL file with XBRL data as a JSON blob, and script tags added.
|
|
537
|
+
:param scriptUrl: The `src` value of the script tag that loads the viewer script.
|
|
538
|
+
:param showValidations: True if validation errors should be included in output taxonomy data.
|
|
539
|
+
:return: An iXBRLViewer instance that is ready to be saved.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
self.taxonomyData["prefixes"] = self.nsmap.prefixmap
|
|
543
|
+
self.taxonomyData["roles"] = self.roleMap.prefixmap
|
|
544
|
+
if showValidations:
|
|
545
|
+
self.taxonomyData["validation"] = self.validationErrors()
|
|
546
|
+
|
|
547
|
+
if packageDownloadURL is not None:
|
|
548
|
+
self.taxonomyData["filingDocuments"] = packageDownloadURL
|
|
549
|
+
elif self.fromSingleZIP:
|
|
550
|
+
filingDocZipName = os.path.basename(self.filingDocZipPath)
|
|
551
|
+
self.iv.addFilingDoc(self.filingDocZipPath)
|
|
552
|
+
self.taxonomyData["filingDocuments"] = filingDocZipName
|
|
470
553
|
|
|
471
|
-
if not self.
|
|
554
|
+
if not self.addViewerData(self.iv.files[0], scriptUrl):
|
|
472
555
|
return None
|
|
473
556
|
|
|
474
|
-
|
|
557
|
+
if len(self.iv.files) == 1:
|
|
558
|
+
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
559
|
+
# We should probably preserve the source file extension here.
|
|
560
|
+
self.iv.files[0].filename = 'xbrlviewer.html'
|
|
561
|
+
if self.assets:
|
|
562
|
+
self.iv.addReportAssets(self.assets)
|
|
563
|
+
if self.reportZip:
|
|
564
|
+
self.iv.reportZip = self.reportZip
|
|
565
|
+
return self.iv
|
|
475
566
|
|
|
476
567
|
|
|
477
568
|
class iXBRLViewerFile:
|
|
478
569
|
|
|
479
|
-
def __init__(self, filename, xmlDocument):
|
|
570
|
+
def __init__(self, filename: str, xmlDocument: etree._ElementTree[etree._Element]) -> None:
|
|
480
571
|
self.filename = filename
|
|
481
|
-
self.xmlDocument = xmlDocument
|
|
572
|
+
self.xmlDocument: etree._ElementTree[etree._Element] = deepcopy(xmlDocument)
|
|
573
|
+
# deepcopy does not retain the Python proxies, so iterating the node
|
|
574
|
+
# tree during serialization will create new ones. However, the original
|
|
575
|
+
# ModelObjectFactory is still referenced, and that references a
|
|
576
|
+
# ModelXbrl that will potentially be closed by the time we serialize.
|
|
577
|
+
# Serialization only requires standard XML features, so the default
|
|
578
|
+
# lxml.etree classes (and thus lookup) are fine.
|
|
579
|
+
if self.xmlDocument.parser is not None:
|
|
580
|
+
self.xmlDocument.parser.set_element_class_lookup(etree.ElementDefaultClassLookup())
|
|
482
581
|
|
|
483
582
|
|
|
484
583
|
class iXBRLViewer:
|
|
485
584
|
|
|
486
|
-
def __init__(self,
|
|
487
|
-
self.
|
|
488
|
-
self.
|
|
489
|
-
self.
|
|
490
|
-
|
|
491
|
-
self.
|
|
585
|
+
def __init__(self, cntlr: Cntlr) -> None:
|
|
586
|
+
self.reportZip: str | None = None
|
|
587
|
+
self.filesByFilename: dict[str, iXBRLViewerFile] = {}
|
|
588
|
+
self.filingDocuments: str | None = None
|
|
589
|
+
self.cntlr = cntlr
|
|
590
|
+
self.assets: list[str] = []
|
|
492
591
|
|
|
493
|
-
def
|
|
592
|
+
def addReportAssets(self, assets: list[str]) -> None:
|
|
593
|
+
self.assets.extend(assets)
|
|
594
|
+
|
|
595
|
+
def addFile(self, ivf: iXBRLViewerFile) -> None:
|
|
596
|
+
# Overwrite previous occurrences of the same document, because it may
|
|
597
|
+
# have had more IDs added to it by subsequent target documents.
|
|
598
|
+
self.filesByFilename[ivf.filename] = ivf
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def files(self) -> list[iXBRLViewerFile]:
|
|
602
|
+
return list(self.filesByFilename.values())
|
|
603
|
+
|
|
604
|
+
def addFilingDoc(self, filingDocuments: str) -> None:
|
|
494
605
|
self.filingDocuments = filingDocuments
|
|
495
606
|
|
|
496
|
-
def save(self, destination:
|
|
607
|
+
def save(self, destination: io.BytesIO | str, zipOutput: bool = False, copyScriptPath: Path | None = None) -> None:
|
|
497
608
|
"""
|
|
498
609
|
Save the iXBRL viewer.
|
|
499
610
|
:param destination: The target that viewer data/files will be written to (path to file/directory, or a file object itself).
|
|
@@ -502,82 +613,113 @@ class iXBRLViewer:
|
|
|
502
613
|
"""
|
|
503
614
|
if isinstance(destination, io.BytesIO) or zipOutput: # zip output stream
|
|
504
615
|
# zipfile may be cumulatively added to by inline extraction, EdgarRenderer etc
|
|
616
|
+
filepath: io.BytesIO | str
|
|
617
|
+
fileMode: Literal['a', 'w']
|
|
505
618
|
if isinstance(destination, io.BytesIO):
|
|
506
|
-
|
|
619
|
+
filepath = destination
|
|
507
620
|
fileMode = 'a'
|
|
508
621
|
destination = os.sep
|
|
509
622
|
elif os.path.isdir(destination):
|
|
510
|
-
|
|
623
|
+
filepath = os.path.join(
|
|
624
|
+
destination,
|
|
625
|
+
f'{os.path.splitext(os.path.basename(self.files[0].filename))[0]}.zip',
|
|
626
|
+
)
|
|
511
627
|
fileMode = 'w'
|
|
512
628
|
elif destination.endswith(os.sep):
|
|
513
629
|
# Looks like a directory, but isn't one
|
|
514
|
-
self.
|
|
630
|
+
self.cntlr.addToLog(
|
|
631
|
+
f"Directory {destination} does not exist",
|
|
632
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
633
|
+
)
|
|
515
634
|
return
|
|
516
635
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
517
636
|
# Directory part of filename doesn't exist
|
|
518
|
-
self.
|
|
637
|
+
self.cntlr.addToLog(
|
|
638
|
+
f"Directory {os.path.dirname(os.path.abspath(destination))} does not exist",
|
|
639
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
640
|
+
)
|
|
519
641
|
return
|
|
520
642
|
elif not destination.endswith('.zip'):
|
|
521
643
|
# File extension isn't a zip
|
|
522
|
-
self.
|
|
644
|
+
self.cntlr.addToLog(
|
|
645
|
+
f"File extension {os.path.splitext(destination)[0]} is not a zip",
|
|
646
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
647
|
+
)
|
|
523
648
|
return
|
|
524
649
|
else:
|
|
525
|
-
|
|
650
|
+
filepath = destination
|
|
526
651
|
fileMode = 'w'
|
|
527
652
|
|
|
528
|
-
with zipfile.ZipFile(
|
|
653
|
+
with zipfile.ZipFile(filepath, fileMode, zipfile.ZIP_DEFLATED, allowZip64=True) as zout:
|
|
529
654
|
for f in self.files:
|
|
530
|
-
self.
|
|
655
|
+
self.cntlr.addToLog(f"Saving in output zip {f.filename}", messageCode=INFO_MESSAGE_CODE)
|
|
531
656
|
with zout.open(f.filename, "w") as fout:
|
|
532
657
|
writer = XHTMLSerializer(fout)
|
|
533
658
|
writer.serialize(f.xmlDocument)
|
|
534
659
|
if self.filingDocuments:
|
|
535
660
|
filename = os.path.basename(self.filingDocuments)
|
|
536
|
-
self.
|
|
661
|
+
self.cntlr.addToLog(f"Writing {filename}", messageCode=INFO_MESSAGE_CODE)
|
|
537
662
|
zout.write(self.filingDocuments, filename)
|
|
538
663
|
if copyScriptPath is not None:
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
zout.write(scriptSrc, os.path.basename(copyScriptPath))
|
|
664
|
+
self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
|
|
665
|
+
zout.write(copyScriptPath, copyScriptPath.name)
|
|
542
666
|
elif os.path.isdir(destination):
|
|
543
667
|
# If output is a directory, write each file in the doc set to that
|
|
544
668
|
# directory using its existing filename
|
|
545
669
|
for f in self.files:
|
|
546
670
|
filename = os.path.join(destination, f.filename)
|
|
547
|
-
self.
|
|
671
|
+
self.cntlr.addToLog(f"Writing {filename}", messageCode=INFO_MESSAGE_CODE)
|
|
548
672
|
with open(filename, "wb") as fout:
|
|
549
673
|
writer = XHTMLSerializer(fout)
|
|
550
674
|
writer.serialize(f.xmlDocument)
|
|
551
675
|
if self.filingDocuments:
|
|
552
676
|
filename = os.path.basename(self.filingDocuments)
|
|
553
|
-
self.
|
|
677
|
+
self.cntlr.addToLog(f"Writing {filename}", messageCode=INFO_MESSAGE_CODE)
|
|
554
678
|
shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
|
|
679
|
+
if self.assets and self.reportZip is not None:
|
|
680
|
+
with zipfile.ZipFile(self.reportZip) as z:
|
|
681
|
+
for asset in self.assets:
|
|
682
|
+
fileName = os.path.basename(asset)
|
|
683
|
+
path = os.path.join(destination, fileName)
|
|
684
|
+
self.cntlr.addToLog(f"Writing {asset}", messageCode=INFO_MESSAGE_CODE)
|
|
685
|
+
with z.open(asset) as zf, open(path, 'wb') as assetFile:
|
|
686
|
+
shutil.copyfileobj(zf, assetFile)
|
|
687
|
+
|
|
555
688
|
if copyScriptPath is not None:
|
|
556
|
-
self._copyScript(destination, copyScriptPath)
|
|
689
|
+
self._copyScript(Path(destination), copyScriptPath)
|
|
557
690
|
else:
|
|
558
691
|
if len(self.files) > 1:
|
|
559
|
-
self.
|
|
692
|
+
self.cntlr.addToLog(
|
|
693
|
+
"More than one file in input, but output is not a directory",
|
|
694
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
695
|
+
)
|
|
560
696
|
elif destination.endswith(os.sep):
|
|
561
697
|
# Looks like a directory, but isn't one
|
|
562
|
-
self.
|
|
698
|
+
self.cntlr.addToLog(
|
|
699
|
+
f"Directory {destination} does not exist",
|
|
700
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
701
|
+
)
|
|
563
702
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
564
703
|
# Directory part of filename doesn't exist
|
|
565
|
-
self.
|
|
704
|
+
self.cntlr.addToLog(
|
|
705
|
+
f"Directory {os.path.dirname(os.path.abspath(destination))} does not exist",
|
|
706
|
+
messageCode=ERROR_MESSAGE_CODE,
|
|
707
|
+
)
|
|
566
708
|
else:
|
|
567
|
-
self.
|
|
709
|
+
self.cntlr.addToLog(f"Writing {destination}", messageCode=INFO_MESSAGE_CODE)
|
|
568
710
|
with open(destination, "wb") as fout:
|
|
569
711
|
writer = XHTMLSerializer(fout)
|
|
570
712
|
writer.serialize(self.files[0].xmlDocument)
|
|
571
713
|
if self.filingDocuments:
|
|
572
714
|
filename = os.path.basename(self.filingDocuments)
|
|
573
|
-
self.
|
|
715
|
+
self.cntlr.addToLog(f"Writing {filename}", messageCode=INFO_MESSAGE_CODE)
|
|
574
716
|
shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
|
|
575
717
|
if copyScriptPath is not None:
|
|
576
|
-
outDirectory =
|
|
718
|
+
outDirectory = Path(destination).parent
|
|
577
719
|
self._copyScript(outDirectory, copyScriptPath)
|
|
578
720
|
|
|
579
|
-
def _copyScript(self,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
721
|
+
def _copyScript(self, destDirectory: Path, scriptPath: Path) -> None:
|
|
722
|
+
scriptDest = destDirectory / scriptPath.name
|
|
723
|
+
if scriptPath != scriptDest:
|
|
724
|
+
self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
|
|
725
|
+
shutil.copy2(scriptPath, scriptDest)
|