ixbrl-viewer 1.4.21__py3-none-any.whl → 1.4.50__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.
Potentially problematic release.
This version of ixbrl-viewer might be problematic. Click here for more details.
- iXBRLViewerPlugin/__init__.py +77 -49
- iXBRLViewerPlugin/_version.py +2 -2
- iXBRLViewerPlugin/constants.py +86 -1
- iXBRLViewerPlugin/featureConfig.py +4 -1
- iXBRLViewerPlugin/iXBRLViewer.py +210 -137
- iXBRLViewerPlugin/plugin.py +7 -0
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js.LICENSE.txt +9 -2
- iXBRLViewerPlugin/viewer/i18next-parser.config.js +1 -1
- iXBRLViewerPlugin/viewer/src/data/utr.json +1 -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 +1 -1
- iXBRLViewerPlugin/viewer/src/html/inspector.html +324 -211
- 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/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 +56 -24
- 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 +70 -37
- iXBRLViewerPlugin/viewer/src/icons/dark-mode.svg +4 -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 +3 -2
- iXBRLViewerPlugin/viewer/src/js/aspect.js +18 -10
- iXBRLViewerPlugin/viewer/src/js/aspect.test.js +2 -2
- iXBRLViewerPlugin/viewer/src/js/balance.js +14 -0
- iXBRLViewerPlugin/viewer/src/js/calculation.js +45 -34
- iXBRLViewerPlugin/viewer/src/js/calculationInspector.js +4 -7
- iXBRLViewerPlugin/viewer/src/js/chart.js +23 -21
- iXBRLViewerPlugin/viewer/src/js/concept.js +30 -3
- iXBRLViewerPlugin/viewer/src/js/concept.test.js +23 -2
- 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/fact.js +40 -7
- iXBRLViewerPlugin/viewer/src/js/fact.test.js +3 -0
- iXBRLViewerPlugin/viewer/src/js/index.js +11 -3
- iXBRLViewerPlugin/viewer/src/js/inspector.js +560 -160
- iXBRLViewerPlugin/viewer/src/js/inspector.test.js +1 -2
- iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.js +129 -31
- iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.test.js +133 -20
- iXBRLViewerPlugin/viewer/src/js/ixnode.js +1 -1
- iXBRLViewerPlugin/viewer/src/js/menu.js +25 -7
- iXBRLViewerPlugin/viewer/src/js/number-matcher.js +2 -2
- iXBRLViewerPlugin/viewer/src/js/outline.js +2 -4
- iXBRLViewerPlugin/viewer/src/js/period.js +0 -1
- iXBRLViewerPlugin/viewer/src/js/report.js +68 -13
- iXBRLViewerPlugin/viewer/src/js/report.test.js +77 -6
- iXBRLViewerPlugin/viewer/src/js/reportset.js +33 -3
- iXBRLViewerPlugin/viewer/src/js/reportset.test.js +32 -6
- iXBRLViewerPlugin/viewer/src/js/search.js +61 -35
- iXBRLViewerPlugin/viewer/src/js/search.test.js +8 -5
- iXBRLViewerPlugin/viewer/src/js/summary.js +28 -2
- iXBRLViewerPlugin/viewer/src/js/summary.test.js +52 -14
- iXBRLViewerPlugin/viewer/src/js/tableExport.js +3 -3
- iXBRLViewerPlugin/viewer/src/js/taxonomynamer.js +34 -0
- iXBRLViewerPlugin/viewer/src/js/taxonomynamer.test.js +32 -0
- iXBRLViewerPlugin/viewer/src/js/theme.js +49 -0
- iXBRLViewerPlugin/viewer/src/js/unit.js +73 -2
- iXBRLViewerPlugin/viewer/src/js/unit.test.js +14 -3
- iXBRLViewerPlugin/viewer/src/js/util.js +21 -18
- iXBRLViewerPlugin/viewer/src/js/util.test.js +1 -0
- iXBRLViewerPlugin/viewer/src/js/utr.js +27 -0
- iXBRLViewerPlugin/viewer/src/js/viewer.js +40 -29
- iXBRLViewerPlugin/viewer/src/js/viewerOptions.js +0 -2
- iXBRLViewerPlugin/viewer/src/less/accordian.less +8 -4
- iXBRLViewerPlugin/viewer/src/less/block-list.less +12 -6
- iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +2 -2
- iXBRLViewerPlugin/viewer/src/less/chart.less +8 -5
- iXBRLViewerPlugin/viewer/src/less/colours-dark-mode.less +40 -0
- iXBRLViewerPlugin/viewer/src/less/colours.less +28 -21
- iXBRLViewerPlugin/viewer/src/less/common.less +1 -1
- iXBRLViewerPlugin/viewer/src/less/components.less +3 -3
- iXBRLViewerPlugin/viewer/src/less/core.less +2 -0
- iXBRLViewerPlugin/viewer/src/less/dialog.less +13 -10
- iXBRLViewerPlugin/viewer/src/less/form-controls.less +33 -11
- iXBRLViewerPlugin/viewer/src/less/inspector.less +556 -300
- 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 +5 -5
- iXBRLViewerPlugin/viewer/src/less/text-mixins.less +2 -1
- iXBRLViewerPlugin/viewer/src/less/validation-report.less +1 -1
- iXBRLViewerPlugin/viewer/src/less/viewer.less +30 -18
- iXBRLViewerPlugin/viewer/webpack.common.js +19 -9
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/METADATA +41 -14
- ixbrl_viewer-1.4.50.dist-info/RECORD +197 -0
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/WHEEL +1 -1
- tests/puppeteer/framework/page_objects/doc_frame.js +3 -3
- tests/puppeteer/framework/page_objects/fact_details_panel.js +29 -1
- tests/puppeteer/puppeteer_test_run_via_intellij.jpg +0 -0
- tests/puppeteer/tests/fact_properties.test.js +10 -4
- tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +117 -51
- iXBRLViewerPlugin/viewer/src/js/interact.min.js +0 -6
- ixbrl_viewer-1.4.21.dist-info/RECORD +0 -166
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/LICENSE +0 -0
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/NOTICE +0 -0
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/entry_points.txt +0 -0
- {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/top_level.txt +0 -0
iXBRLViewerPlugin/iXBRLViewer.py
CHANGED
|
@@ -13,9 +13,10 @@ import zipfile
|
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
from copy import deepcopy
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
16
17
|
|
|
17
18
|
from arelle import XbrlConst
|
|
18
|
-
from arelle.ModelDocument import Type
|
|
19
|
+
from arelle.ModelDocument import ModelDocument, Type
|
|
19
20
|
from arelle.ModelRelationshipSet import ModelRelationshipSet
|
|
20
21
|
from arelle.ModelValue import QName, INVALIDixVALUE
|
|
21
22
|
from arelle.ModelXbrl import ModelXbrl
|
|
@@ -23,10 +24,10 @@ from arelle.UrlUtil import isHttpUrl
|
|
|
23
24
|
from arelle.ValidateXbrlCalcs import inferredDecimals
|
|
24
25
|
from lxml import etree
|
|
25
26
|
|
|
26
|
-
from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE
|
|
27
|
+
from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE, MANDATORY_FACTS
|
|
27
28
|
from .xhtmlserialize import XHTMLSerializer
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
REPORT_TYPE_EXTENSIONS = ('.xbrl', '.xhtml', '.html', '.htm', '.json')
|
|
30
31
|
UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE = 'unrecognizedLinkbase'
|
|
31
32
|
LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE = {
|
|
32
33
|
XbrlConst.qnLinkCalculationLink: 'calcLinkbase',
|
|
@@ -80,28 +81,52 @@ class NamespaceMap:
|
|
|
80
81
|
class IXBRLViewerBuilderError(Exception):
|
|
81
82
|
pass
|
|
82
83
|
|
|
84
|
+
def isInlineDoc(doc: ModelDocument | None) -> bool:
|
|
85
|
+
return doc is not None and doc.type in {Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET}
|
|
86
|
+
|
|
83
87
|
class IXBRLViewerBuilder:
|
|
84
88
|
|
|
85
|
-
def __init__(self,
|
|
89
|
+
def __init__(self,
|
|
90
|
+
cntlr: Cntlr,
|
|
91
|
+
basenameSuffix: str = '',
|
|
92
|
+
useStubViewer: bool = False,
|
|
93
|
+
features: dict[str, Any] | None = None,
|
|
94
|
+
):
|
|
95
|
+
if features is None:
|
|
96
|
+
features = {}
|
|
97
|
+
featureNames = {c.key for c in FEATURE_CONFIGS}
|
|
98
|
+
for featureName in features:
|
|
99
|
+
assert featureName in featureNames, \
|
|
100
|
+
f'Given feature name `{featureName}` does not match any defined features: {featureNames}'
|
|
101
|
+
self.reportZip = None
|
|
86
102
|
self.nsmap = NamespaceMap()
|
|
87
103
|
self.roleMap = NamespaceMap()
|
|
88
|
-
self.reports = reports
|
|
89
|
-
# Arbitrary ModelXbrl used for logging
|
|
90
|
-
self.logger_model = reports[0]
|
|
91
104
|
self.taxonomyData = {
|
|
92
105
|
"sourceReports": [],
|
|
93
|
-
"features":
|
|
106
|
+
"features": features,
|
|
94
107
|
}
|
|
95
108
|
self.basenameSuffix = basenameSuffix
|
|
96
109
|
self.currentTargetReport = None
|
|
110
|
+
self.useStubViewer = useStubViewer
|
|
111
|
+
self.cntlr = cntlr
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.
|
|
113
|
+
self.idGen = 0
|
|
114
|
+
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
115
|
+
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
116
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
117
|
+
self.roleMap.getPrefix(XbrlConst.summationItem11, "calc11")
|
|
118
|
+
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
119
|
+
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
120
|
+
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
121
|
+
|
|
122
|
+
self.sourceReportsByFiles = dict()
|
|
123
|
+
self.iv = iXBRLViewer(cntlr)
|
|
124
|
+
if self.useStubViewer:
|
|
125
|
+
self.iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
126
|
+
|
|
127
|
+
self.fromSingleZIP = None
|
|
128
|
+
self.reportCount = 0
|
|
129
|
+
self.assets = []
|
|
105
130
|
|
|
106
131
|
def outputFilename(self, filename):
|
|
107
132
|
(base, ext) = os.path.splitext(filename)
|
|
@@ -130,7 +155,7 @@ class IXBRLViewerBuilder:
|
|
|
130
155
|
"""
|
|
131
156
|
return s.replace("<","\\u003C").replace(">","\\u003E").replace("&","\\u0026")
|
|
132
157
|
|
|
133
|
-
def
|
|
158
|
+
def addRoleDefinition(self, report: ModelXbrl, elr):
|
|
134
159
|
prefix = self.roleMap.getPrefix(elr)
|
|
135
160
|
if self.currentTargetReport.setdefault("roleDefs",{}).get(prefix, None) is None:
|
|
136
161
|
rts = report.roleTypes.get(elr, [])
|
|
@@ -151,6 +176,7 @@ class IXBRLViewerBuilder:
|
|
|
151
176
|
for lr in labels:
|
|
152
177
|
l = lr.toModelObject
|
|
153
178
|
conceptData["labels"].setdefault(self.roleMap.getPrefix(l.role),{})[l.xmlLang.lower()] = l.text;
|
|
179
|
+
self.addRoleDefinition(report, l.role)
|
|
154
180
|
|
|
155
181
|
refData = []
|
|
156
182
|
for _refRel in concept.modelXbrl.relationshipSet(XbrlConst.conceptReference).fromModelObject(concept):
|
|
@@ -168,9 +194,15 @@ class IXBRLViewerBuilder:
|
|
|
168
194
|
if concept.isEnumeration:
|
|
169
195
|
conceptData["e"] = True
|
|
170
196
|
|
|
171
|
-
if concept.
|
|
197
|
+
if concept.isTextBlock:
|
|
172
198
|
conceptData['t'] = True
|
|
173
199
|
|
|
200
|
+
if concept.balance is not None:
|
|
201
|
+
conceptData['b'] = concept.balance
|
|
202
|
+
|
|
203
|
+
if concept.type is not None:
|
|
204
|
+
conceptData['dt'] = self.nsmap.qname(concept.type.qname)
|
|
205
|
+
|
|
174
206
|
if concept.isTypedDimension:
|
|
175
207
|
typedDomainElement = concept.typedDomainElement
|
|
176
208
|
if typedDomainElement is not None:
|
|
@@ -190,8 +222,8 @@ class IXBRLViewerBuilder:
|
|
|
190
222
|
|
|
191
223
|
for baseSetKey, baseSetModelLinks in report.baseSets.items():
|
|
192
224
|
arcrole, ELR, linkqname, arcqname = baseSetKey
|
|
193
|
-
if arcrole in (XbrlConst.summationItem, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
|
|
194
|
-
self.
|
|
225
|
+
if arcrole in (XbrlConst.summationItem, XbrlConst.summationItem11, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
|
|
226
|
+
self.addRoleDefinition(report, ELR)
|
|
195
227
|
rr = dict()
|
|
196
228
|
relSet = report.relationshipSet(arcrole, ELR)
|
|
197
229
|
for r in relSet.modelRelationships:
|
|
@@ -210,7 +242,7 @@ class IXBRLViewerBuilder:
|
|
|
210
242
|
return rels
|
|
211
243
|
|
|
212
244
|
def validationErrors(self):
|
|
213
|
-
logHandler = self.
|
|
245
|
+
logHandler = self.cntlr.logHandler
|
|
214
246
|
if getattr(logHandler, "logRecordBuffer") is None:
|
|
215
247
|
raise IXBRLViewerBuilderError("Logging is not configured to use a buffer. Unable to retrieve validation messages")
|
|
216
248
|
|
|
@@ -231,11 +263,14 @@ class IXBRLViewerBuilder:
|
|
|
231
263
|
|
|
232
264
|
self.idGen += 1
|
|
233
265
|
conceptName = self.nsmap.qname(f.qname)
|
|
266
|
+
factList = MANDATORY_FACTS.get(self.taxonomyData["features"].get("mandatory_facts"), [])
|
|
267
|
+
isMandatory = f.qname.localName in factList
|
|
234
268
|
scheme, ident = f.context.entityIdentifier
|
|
235
269
|
|
|
236
270
|
aspects = {
|
|
237
271
|
"c": conceptName,
|
|
238
272
|
"e": self.nsmap.qname(QName(self.nsmap.getPrefix(scheme,"e"), scheme, ident)),
|
|
273
|
+
"m": isMandatory
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
factData = {
|
|
@@ -326,14 +361,13 @@ class IXBRLViewerBuilder:
|
|
|
326
361
|
return numeratorsString
|
|
327
362
|
|
|
328
363
|
def addViewerData(self, viewerFile, scriptUrl):
|
|
329
|
-
viewerFile.xmlDocument = deepcopy(viewerFile.xmlDocument)
|
|
330
364
|
taxonomyDataJSON = self.escapeJSONForScriptTag(json.dumps(self.taxonomyData, indent=1, allow_nan=False))
|
|
331
365
|
|
|
332
366
|
for child in viewerFile.xmlDocument.getroot():
|
|
333
367
|
if child.tag == '{http://www.w3.org/1999/xhtml}body':
|
|
334
368
|
for body_child in child:
|
|
335
369
|
if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
|
|
336
|
-
self.
|
|
370
|
+
self.cntlr.addToLog("File already contains iXBRL viewer", messageCode="error")
|
|
337
371
|
return False
|
|
338
372
|
|
|
339
373
|
child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
|
|
@@ -375,144 +409,174 @@ class IXBRLViewerBuilder:
|
|
|
375
409
|
self.taxonomyData["sourceReports"].append(sourceReport)
|
|
376
410
|
return sourceReport
|
|
377
411
|
|
|
412
|
+
def processModel(
|
|
413
|
+
self,
|
|
414
|
+
report: ModelXbrl
|
|
415
|
+
):
|
|
416
|
+
|
|
417
|
+
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
418
|
+
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
419
|
+
softwareCredits = set()
|
|
420
|
+
for document in report.urlDocs.values():
|
|
421
|
+
if isInlineDoc(document):
|
|
422
|
+
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
423
|
+
softwareCredits.update(matches)
|
|
424
|
+
if softwareCredits:
|
|
425
|
+
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
426
|
+
for f in report.facts:
|
|
427
|
+
if f.isTuple:
|
|
428
|
+
for nestedTupleFact in f.ixIter():
|
|
429
|
+
self.addFact(report, nestedTupleFact)
|
|
430
|
+
else:
|
|
431
|
+
self.addFact(report, f)
|
|
432
|
+
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
433
|
+
|
|
434
|
+
docSetFiles = None
|
|
435
|
+
self.reportCount += 1
|
|
436
|
+
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d) [%s]" % (self.reportCount, self.currentTargetReport["target"]))
|
|
437
|
+
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
438
|
+
# Sort by object index to preserve order in which files were specified.
|
|
439
|
+
xmlDocsByFilename = {
|
|
440
|
+
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
441
|
+
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
442
|
+
if doc.type == Type.INLINEXBRL
|
|
443
|
+
}
|
|
444
|
+
docSetFiles = list(xmlDocsByFilename.keys())
|
|
445
|
+
|
|
446
|
+
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
447
|
+
self.iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
448
|
+
|
|
449
|
+
elif self.useStubViewer:
|
|
450
|
+
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
451
|
+
docSetFiles = [ filename ]
|
|
452
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
453
|
+
|
|
454
|
+
else:
|
|
455
|
+
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
456
|
+
docSetFiles = [ srcFilename ]
|
|
457
|
+
filename = srcFilename
|
|
458
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
459
|
+
docSetKey = frozenset(docSetFiles)
|
|
460
|
+
sourceReport = self.sourceReportsByFiles.get(docSetKey)
|
|
461
|
+
if sourceReport is None:
|
|
462
|
+
sourceReport = self.addSourceReport()
|
|
463
|
+
self.sourceReportsByFiles[docSetKey] = sourceReport
|
|
464
|
+
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
465
|
+
|
|
466
|
+
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
467
|
+
|
|
468
|
+
localDocs = defaultdict(set)
|
|
469
|
+
for path, doc in report.urlDocs.items():
|
|
470
|
+
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
471
|
+
continue
|
|
472
|
+
if doc.type == Type.INLINEXBRL:
|
|
473
|
+
localDocs[doc.basename].add('inline')
|
|
474
|
+
elif doc.type == Type.SCHEMA:
|
|
475
|
+
localDocs[doc.basename].add('schema')
|
|
476
|
+
elif doc.type == Type.LINKBASE:
|
|
477
|
+
linkbaseIdentifed = False
|
|
478
|
+
for child in doc.xmlRootElement.iterchildren():
|
|
479
|
+
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
480
|
+
if linkbaseLocalDocumentsKey is not None:
|
|
481
|
+
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
482
|
+
linkbaseIdentifed = True
|
|
483
|
+
if not linkbaseIdentifed:
|
|
484
|
+
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
485
|
+
self.currentTargetReport["localDocs"] = {
|
|
486
|
+
localDoc: sorted(docTypes)
|
|
487
|
+
for localDoc, docTypes in localDocs.items()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# If we only process a single ZIP, add a download link to it as the
|
|
491
|
+
# "filing documents" on the viewer menu.
|
|
492
|
+
if self.fromSingleZIP is None:
|
|
493
|
+
self.fromSingleZIP = report.modelDocument.filepath.endswith(".zip")
|
|
494
|
+
if self.fromSingleZIP:
|
|
495
|
+
self.filingDocZipPath = os.path.dirname(report.modelDocument.filepath)
|
|
496
|
+
else:
|
|
497
|
+
self.fromSingleZIP = False
|
|
498
|
+
if report.fileSource.isArchive:
|
|
499
|
+
filelist = report.fileSource.fs.filelist
|
|
500
|
+
for file in filelist:
|
|
501
|
+
directory, asset = os.path.split(file.filename)
|
|
502
|
+
if "reports" in directory and asset != '' and not asset.lower().endswith(REPORT_TYPE_EXTENSIONS):
|
|
503
|
+
self.assets.append(file.filename)
|
|
504
|
+
if self.assets:
|
|
505
|
+
self.reportZip = report.fileSource.fs.filename
|
|
506
|
+
|
|
378
507
|
def createViewer(
|
|
379
508
|
self,
|
|
380
509
|
scriptUrl: str = DEFAULT_JS_FILENAME,
|
|
381
|
-
useStubViewer: bool = False,
|
|
382
510
|
showValidations: bool = True,
|
|
383
511
|
packageDownloadURL: str | None = None,
|
|
384
512
|
) -> iXBRLViewer | None:
|
|
385
513
|
"""
|
|
386
514
|
Create an iXBRL file with XBRL data as a JSON blob, and script tags added.
|
|
387
515
|
:param scriptUrl: The `src` value of the script tag that loads the viewer script.
|
|
388
|
-
:param useStubViewer: True if stub document should be included in output.
|
|
389
516
|
:param showValidations: True if validation errors should be included in output taxonomy data.
|
|
390
517
|
:return: An iXBRLViewer instance that is ready to be saved.
|
|
391
518
|
"""
|
|
392
|
-
# This "dts" is only used for logging
|
|
393
|
-
iv = iXBRLViewer(self.reports[0])
|
|
394
|
-
self.idGen = 0
|
|
395
|
-
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
396
|
-
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
397
|
-
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
398
|
-
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
399
|
-
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
400
|
-
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
401
|
-
|
|
402
|
-
sourceReportsByFiles = dict()
|
|
403
|
-
|
|
404
|
-
if useStubViewer:
|
|
405
|
-
iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
406
|
-
|
|
407
|
-
for n, report in enumerate(self.reports):
|
|
408
|
-
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
409
|
-
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
410
|
-
for f in report.facts:
|
|
411
|
-
self.addFact(report, f)
|
|
412
|
-
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
413
|
-
|
|
414
|
-
docSetFiles = None
|
|
415
|
-
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
|
|
416
|
-
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
417
|
-
# Sort by object index to preserve order in which files were specified.
|
|
418
|
-
xmlDocsByFilename = {
|
|
419
|
-
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
420
|
-
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
421
|
-
if doc.type == Type.INLINEXBRL
|
|
422
|
-
}
|
|
423
|
-
docSetFiles = list(xmlDocsByFilename.keys())
|
|
424
|
-
|
|
425
|
-
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
426
|
-
iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
427
|
-
|
|
428
|
-
elif useStubViewer:
|
|
429
|
-
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
430
|
-
docSetFiles = [ filename ]
|
|
431
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
432
|
-
|
|
433
|
-
else:
|
|
434
|
-
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
435
|
-
docSetFiles = [ srcFilename ]
|
|
436
|
-
if len(self.reports) == 1:
|
|
437
|
-
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
438
|
-
filename = "xbrlviewer.html"
|
|
439
|
-
else:
|
|
440
|
-
# Otherwise, preserve filenames
|
|
441
|
-
filename = srcFilename
|
|
442
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
443
|
-
|
|
444
|
-
docSetKey = frozenset(docSetFiles)
|
|
445
|
-
sourceReport = sourceReportsByFiles.get(docSetKey)
|
|
446
|
-
if sourceReport is None:
|
|
447
|
-
sourceReport = self.addSourceReport()
|
|
448
|
-
sourceReportsByFiles[docSetKey] = sourceReport
|
|
449
|
-
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
450
|
-
|
|
451
|
-
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
452
|
-
|
|
453
|
-
localDocs = defaultdict(set)
|
|
454
|
-
for path, doc in report.urlDocs.items():
|
|
455
|
-
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
456
|
-
continue
|
|
457
|
-
if doc.type == Type.INLINEXBRL:
|
|
458
|
-
localDocs[doc.basename].add('inline')
|
|
459
|
-
elif doc.type == Type.SCHEMA:
|
|
460
|
-
localDocs[doc.basename].add('schema')
|
|
461
|
-
elif doc.type == Type.LINKBASE:
|
|
462
|
-
linkbaseIdentifed = False
|
|
463
|
-
for child in doc.xmlRootElement.iterchildren():
|
|
464
|
-
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
465
|
-
if linkbaseLocalDocumentsKey is not None:
|
|
466
|
-
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
467
|
-
linkbaseIdentifed = True
|
|
468
|
-
if not linkbaseIdentifed:
|
|
469
|
-
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
470
|
-
self.currentTargetReport["localDocs"] = {
|
|
471
|
-
localDoc: sorted(docTypes)
|
|
472
|
-
for localDoc, docTypes in localDocs.items()
|
|
473
|
-
}
|
|
474
519
|
|
|
475
520
|
self.taxonomyData["prefixes"] = self.nsmap.prefixmap
|
|
476
521
|
self.taxonomyData["roles"] = self.roleMap.prefixmap
|
|
477
|
-
|
|
478
522
|
if showValidations:
|
|
479
523
|
self.taxonomyData["validation"] = self.validationErrors()
|
|
480
524
|
|
|
481
525
|
if packageDownloadURL is not None:
|
|
482
526
|
self.taxonomyData["filingDocuments"] = packageDownloadURL
|
|
483
|
-
elif
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
iv.addFilingDoc(filingDocZipPath)
|
|
527
|
+
elif self.fromSingleZIP:
|
|
528
|
+
filingDocZipName = os.path.basename(self.filingDocZipPath)
|
|
529
|
+
self.iv.addFilingDoc(self.filingDocZipPath)
|
|
487
530
|
self.taxonomyData["filingDocuments"] = filingDocZipName
|
|
488
531
|
|
|
489
|
-
if not self.addViewerData(iv.files[0], scriptUrl):
|
|
532
|
+
if not self.addViewerData(self.iv.files[0], scriptUrl):
|
|
490
533
|
return None
|
|
491
534
|
|
|
492
|
-
|
|
535
|
+
if len(self.iv.files) == 1:
|
|
536
|
+
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
537
|
+
# We should probably preserve the source file extension here.
|
|
538
|
+
self.iv.files[0].filename = 'xbrlviewer.html'
|
|
539
|
+
if self.assets:
|
|
540
|
+
self.iv.addReportAssets(self.assets)
|
|
541
|
+
if self.reportZip:
|
|
542
|
+
self.iv.reportZip = self.reportZip
|
|
543
|
+
return self.iv
|
|
493
544
|
|
|
494
545
|
|
|
495
546
|
class iXBRLViewerFile:
|
|
496
547
|
|
|
497
548
|
def __init__(self, filename, xmlDocument):
|
|
498
549
|
self.filename = filename
|
|
499
|
-
self.xmlDocument = xmlDocument
|
|
550
|
+
self.xmlDocument = deepcopy(xmlDocument)
|
|
551
|
+
# deepcopy does not retain the Python proxies, so iterating the node
|
|
552
|
+
# tree during serialization will create new ones. However, the original
|
|
553
|
+
# ModelObjectFactory is still referenced, and that references a
|
|
554
|
+
# ModelXbrl that will potentially be closed by the time we serialize.
|
|
555
|
+
# Serialization only requires standard XML features, so the default
|
|
556
|
+
# lxml.etree classes (and thus lookup) are fine.
|
|
557
|
+
self.xmlDocument.parser.set_element_class_lookup(etree.ElementDefaultClassLookup())
|
|
500
558
|
|
|
501
559
|
|
|
502
560
|
class iXBRLViewer:
|
|
503
561
|
|
|
504
|
-
def __init__(self,
|
|
505
|
-
self.
|
|
562
|
+
def __init__(self, cntlr: Cntlr):
|
|
563
|
+
self.reportZip = None
|
|
564
|
+
self.filesByFilename = dict()
|
|
506
565
|
self.filingDocuments = None
|
|
507
|
-
|
|
508
|
-
self.
|
|
509
|
-
|
|
566
|
+
self.cntlr = cntlr
|
|
567
|
+
self.assets = []
|
|
568
|
+
|
|
569
|
+
def addReportAssets(self, assets):
|
|
570
|
+
self.assets.extend(assets)
|
|
510
571
|
|
|
511
572
|
def addFile(self, ivf):
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
self.
|
|
515
|
-
|
|
573
|
+
# Overwrite previous occurrences of the same document, because it may
|
|
574
|
+
# have had more IDs added to it by subsequent target documents.
|
|
575
|
+
self.filesByFilename[ivf.filename] = ivf
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def files(self):
|
|
579
|
+
return list(self.filesByFilename.values())
|
|
516
580
|
|
|
517
581
|
def addFilingDoc(self, filingDocuments):
|
|
518
582
|
self.filingDocuments = filingDocuments
|
|
@@ -535,15 +599,15 @@ class iXBRLViewer:
|
|
|
535
599
|
fileMode = 'w'
|
|
536
600
|
elif destination.endswith(os.sep):
|
|
537
601
|
# Looks like a directory, but isn't one
|
|
538
|
-
self.
|
|
602
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
539
603
|
return
|
|
540
604
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
541
605
|
# Directory part of filename doesn't exist
|
|
542
|
-
self.
|
|
606
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
543
607
|
return
|
|
544
608
|
elif not destination.endswith('.zip'):
|
|
545
609
|
# File extension isn't a zip
|
|
546
|
-
self.
|
|
610
|
+
self.cntlr.addToLog("File extension %s is not a zip" % os.path.splitext(destination)[0], messageCode=ERROR_MESSAGE_CODE)
|
|
547
611
|
return
|
|
548
612
|
else:
|
|
549
613
|
file = destination
|
|
@@ -551,49 +615,58 @@ class iXBRLViewer:
|
|
|
551
615
|
|
|
552
616
|
with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
|
|
553
617
|
for f in self.files:
|
|
554
|
-
self.
|
|
618
|
+
self.cntlr.addToLog("Saving in output zip %s" % f.filename, messageCode=INFO_MESSAGE_CODE)
|
|
555
619
|
with zout.open(f.filename, "w") as fout:
|
|
556
620
|
writer = XHTMLSerializer(fout)
|
|
557
621
|
writer.serialize(f.xmlDocument)
|
|
558
622
|
if self.filingDocuments:
|
|
559
623
|
filename = os.path.basename(self.filingDocuments)
|
|
560
|
-
self.
|
|
624
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
561
625
|
zout.write(self.filingDocuments, filename)
|
|
562
626
|
if copyScriptPath is not None:
|
|
563
|
-
self.
|
|
627
|
+
self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
|
|
564
628
|
zout.write(copyScriptPath, copyScriptPath.name)
|
|
565
629
|
elif os.path.isdir(destination):
|
|
566
630
|
# If output is a directory, write each file in the doc set to that
|
|
567
631
|
# directory using its existing filename
|
|
568
632
|
for f in self.files:
|
|
569
633
|
filename = os.path.join(destination, f.filename)
|
|
570
|
-
self.
|
|
634
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
571
635
|
with open(filename, "wb") as fout:
|
|
572
636
|
writer = XHTMLSerializer(fout)
|
|
573
637
|
writer.serialize(f.xmlDocument)
|
|
574
638
|
if self.filingDocuments:
|
|
575
639
|
filename = os.path.basename(self.filingDocuments)
|
|
576
|
-
self.
|
|
640
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
577
641
|
shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
|
|
642
|
+
if self.assets:
|
|
643
|
+
with zipfile.ZipFile(self.reportZip) as z:
|
|
644
|
+
for asset in self.assets:
|
|
645
|
+
fileName = os.path.basename(asset)
|
|
646
|
+
path = os.path.join(destination, fileName)
|
|
647
|
+
self.cntlr.addToLog("Writing %s" % asset, messageCode=INFO_MESSAGE_CODE)
|
|
648
|
+
with z.open(asset) as zf, open(path, 'wb') as f:
|
|
649
|
+
shutil.copyfileobj(zf, f)
|
|
650
|
+
|
|
578
651
|
if copyScriptPath is not None:
|
|
579
652
|
self._copyScript(Path(destination), copyScriptPath)
|
|
580
653
|
else:
|
|
581
654
|
if len(self.files) > 1:
|
|
582
|
-
self.
|
|
655
|
+
self.cntlr.addToLog("More than one file in input, but output is not a directory", messageCode=ERROR_MESSAGE_CODE)
|
|
583
656
|
elif destination.endswith(os.sep):
|
|
584
657
|
# Looks like a directory, but isn't one
|
|
585
|
-
self.
|
|
658
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
586
659
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
587
660
|
# Directory part of filename doesn't exist
|
|
588
|
-
self.
|
|
661
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
589
662
|
else:
|
|
590
|
-
self.
|
|
663
|
+
self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
|
|
591
664
|
with open(destination, "wb") as fout:
|
|
592
665
|
writer = XHTMLSerializer(fout)
|
|
593
666
|
writer.serialize(self.files[0].xmlDocument)
|
|
594
667
|
if self.filingDocuments:
|
|
595
668
|
filename = os.path.basename(self.filingDocuments)
|
|
596
|
-
self.
|
|
669
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
597
670
|
shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
|
|
598
671
|
if copyScriptPath is not None:
|
|
599
672
|
outDirectory = Path(destination).parent
|
|
@@ -602,5 +675,5 @@ class iXBRLViewer:
|
|
|
602
675
|
def _copyScript(self, destDirectory: Path, scriptPath: Path):
|
|
603
676
|
scriptDest = destDirectory / scriptPath.name
|
|
604
677
|
if scriptPath != scriptDest:
|
|
605
|
-
self.
|
|
678
|
+
self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
|
|
606
679
|
shutil.copy2(scriptPath, scriptDest)
|