ixbrl-viewer 1.4.20__py3-none-any.whl → 1.4.49__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 +202 -131
- 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 +27 -2
- 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 +41 -8
- iXBRLViewerPlugin/viewer/src/js/fact.test.js +47 -13
- 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 +22 -0
- iXBRLViewerPlugin/viewer/src/js/summary.test.js +50 -13
- 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.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/METADATA +41 -14
- ixbrl_viewer-1.4.49.dist-info/RECORD +197 -0
- {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/WHEEL +1 -1
- tests/puppeteer/framework/page_objects/doc_frame.js +3 -3
- tests/puppeteer/framework/page_objects/fact_details_panel.js +28 -0
- 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.20.dist-info/RECORD +0 -166
- {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/LICENSE +0 -0
- {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/NOTICE +0 -0
- {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/entry_points.txt +0 -0
- {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.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,138 +409,166 @@ 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,
|
|
562
|
+
def __init__(self, cntlr: Cntlr):
|
|
563
|
+
self.reportZip = None
|
|
505
564
|
self.files = []
|
|
506
565
|
self.filingDocuments = None
|
|
507
|
-
|
|
508
|
-
self.logger_model = logger_model
|
|
566
|
+
self.cntlr = cntlr
|
|
509
567
|
self.filenames = set()
|
|
568
|
+
self.assets = []
|
|
569
|
+
|
|
570
|
+
def addReportAssets(self, assets):
|
|
571
|
+
self.assets.extend(assets)
|
|
510
572
|
|
|
511
573
|
def addFile(self, ivf):
|
|
512
574
|
if ivf.filename in self.filenames:
|
|
@@ -535,15 +597,15 @@ class iXBRLViewer:
|
|
|
535
597
|
fileMode = 'w'
|
|
536
598
|
elif destination.endswith(os.sep):
|
|
537
599
|
# Looks like a directory, but isn't one
|
|
538
|
-
self.
|
|
600
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
539
601
|
return
|
|
540
602
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
541
603
|
# Directory part of filename doesn't exist
|
|
542
|
-
self.
|
|
604
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
543
605
|
return
|
|
544
606
|
elif not destination.endswith('.zip'):
|
|
545
607
|
# File extension isn't a zip
|
|
546
|
-
self.
|
|
608
|
+
self.cntlr.addToLog("File extension %s is not a zip" % os.path.splitext(destination)[0], messageCode=ERROR_MESSAGE_CODE)
|
|
547
609
|
return
|
|
548
610
|
else:
|
|
549
611
|
file = destination
|
|
@@ -551,49 +613,58 @@ class iXBRLViewer:
|
|
|
551
613
|
|
|
552
614
|
with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
|
|
553
615
|
for f in self.files:
|
|
554
|
-
self.
|
|
616
|
+
self.cntlr.addToLog("Saving in output zip %s" % f.filename, messageCode=INFO_MESSAGE_CODE)
|
|
555
617
|
with zout.open(f.filename, "w") as fout:
|
|
556
618
|
writer = XHTMLSerializer(fout)
|
|
557
619
|
writer.serialize(f.xmlDocument)
|
|
558
620
|
if self.filingDocuments:
|
|
559
621
|
filename = os.path.basename(self.filingDocuments)
|
|
560
|
-
self.
|
|
622
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
561
623
|
zout.write(self.filingDocuments, filename)
|
|
562
624
|
if copyScriptPath is not None:
|
|
563
|
-
self.
|
|
625
|
+
self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
|
|
564
626
|
zout.write(copyScriptPath, copyScriptPath.name)
|
|
565
627
|
elif os.path.isdir(destination):
|
|
566
628
|
# If output is a directory, write each file in the doc set to that
|
|
567
629
|
# directory using its existing filename
|
|
568
630
|
for f in self.files:
|
|
569
631
|
filename = os.path.join(destination, f.filename)
|
|
570
|
-
self.
|
|
632
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
571
633
|
with open(filename, "wb") as fout:
|
|
572
634
|
writer = XHTMLSerializer(fout)
|
|
573
635
|
writer.serialize(f.xmlDocument)
|
|
574
636
|
if self.filingDocuments:
|
|
575
637
|
filename = os.path.basename(self.filingDocuments)
|
|
576
|
-
self.
|
|
638
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
577
639
|
shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
|
|
640
|
+
if self.assets:
|
|
641
|
+
with zipfile.ZipFile(self.reportZip) as z:
|
|
642
|
+
for asset in self.assets:
|
|
643
|
+
fileName = os.path.basename(asset)
|
|
644
|
+
path = os.path.join(destination, fileName)
|
|
645
|
+
self.cntlr.addToLog("Writing %s" % asset, messageCode=INFO_MESSAGE_CODE)
|
|
646
|
+
with z.open(asset) as zf, open(path, 'wb') as f:
|
|
647
|
+
shutil.copyfileobj(zf, f)
|
|
648
|
+
|
|
578
649
|
if copyScriptPath is not None:
|
|
579
650
|
self._copyScript(Path(destination), copyScriptPath)
|
|
580
651
|
else:
|
|
581
652
|
if len(self.files) > 1:
|
|
582
|
-
self.
|
|
653
|
+
self.cntlr.addToLog("More than one file in input, but output is not a directory", messageCode=ERROR_MESSAGE_CODE)
|
|
583
654
|
elif destination.endswith(os.sep):
|
|
584
655
|
# Looks like a directory, but isn't one
|
|
585
|
-
self.
|
|
656
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
586
657
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
587
658
|
# Directory part of filename doesn't exist
|
|
588
|
-
self.
|
|
659
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
589
660
|
else:
|
|
590
|
-
self.
|
|
661
|
+
self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
|
|
591
662
|
with open(destination, "wb") as fout:
|
|
592
663
|
writer = XHTMLSerializer(fout)
|
|
593
664
|
writer.serialize(self.files[0].xmlDocument)
|
|
594
665
|
if self.filingDocuments:
|
|
595
666
|
filename = os.path.basename(self.filingDocuments)
|
|
596
|
-
self.
|
|
667
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
597
668
|
shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
|
|
598
669
|
if copyScriptPath is not None:
|
|
599
670
|
outDirectory = Path(destination).parent
|
|
@@ -602,5 +673,5 @@ class iXBRLViewer:
|
|
|
602
673
|
def _copyScript(self, destDirectory: Path, scriptPath: Path):
|
|
603
674
|
scriptDest = destDirectory / scriptPath.name
|
|
604
675
|
if scriptPath != scriptDest:
|
|
605
|
-
self.
|
|
676
|
+
self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
|
|
606
677
|
shutil.copy2(scriptPath, scriptDest)
|