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.

Files changed (114) hide show
  1. iXBRLViewerPlugin/__init__.py +77 -49
  2. iXBRLViewerPlugin/_version.py +2 -2
  3. iXBRLViewerPlugin/constants.py +86 -1
  4. iXBRLViewerPlugin/featureConfig.py +4 -1
  5. iXBRLViewerPlugin/iXBRLViewer.py +202 -131
  6. iXBRLViewerPlugin/plugin.py +7 -0
  7. iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
  8. iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js.LICENSE.txt +9 -2
  9. iXBRLViewerPlugin/viewer/i18next-parser.config.js +1 -1
  10. iXBRLViewerPlugin/viewer/src/data/utr.json +1 -0
  11. iXBRLViewerPlugin/viewer/src/html/fact-details.html +69 -38
  12. iXBRLViewerPlugin/viewer/src/html/footer-logo.html +4 -0
  13. iXBRLViewerPlugin/viewer/src/html/footnote-details.html +1 -1
  14. iXBRLViewerPlugin/viewer/src/html/inspector.html +324 -211
  15. iXBRLViewerPlugin/viewer/src/i18n/cy/balancetypes.json +1 -0
  16. iXBRLViewerPlugin/viewer/src/i18n/cy/currencies.json +13 -0
  17. iXBRLViewerPlugin/viewer/src/i18n/cy/datatypes.json +9 -0
  18. iXBRLViewerPlugin/viewer/src/i18n/cy/labelroles.json +24 -0
  19. iXBRLViewerPlugin/viewer/src/i18n/cy/referenceparts.json +10 -0
  20. iXBRLViewerPlugin/viewer/src/i18n/cy/scale.json +16 -0
  21. iXBRLViewerPlugin/viewer/src/i18n/cy/tooltips.json +17 -0
  22. iXBRLViewerPlugin/viewer/src/i18n/cy/translation.json +179 -0
  23. iXBRLViewerPlugin/viewer/src/i18n/en/balancetypes.json +4 -0
  24. iXBRLViewerPlugin/viewer/src/i18n/en/datatypes.json +10 -0
  25. iXBRLViewerPlugin/viewer/src/i18n/en/labelroles.json +4 -0
  26. iXBRLViewerPlugin/viewer/src/i18n/en/scale.json +16 -0
  27. iXBRLViewerPlugin/viewer/src/i18n/en/tooltips.json +17 -0
  28. iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +56 -24
  29. iXBRLViewerPlugin/viewer/src/i18n/es/balancetypes.json +4 -0
  30. iXBRLViewerPlugin/viewer/src/i18n/es/datatypes.json +10 -0
  31. iXBRLViewerPlugin/viewer/src/i18n/es/labelroles.json +24 -0
  32. iXBRLViewerPlugin/viewer/src/i18n/es/scale.json +16 -0
  33. iXBRLViewerPlugin/viewer/src/i18n/es/tooltips.json +17 -0
  34. iXBRLViewerPlugin/viewer/src/i18n/es/translation.json +70 -37
  35. iXBRLViewerPlugin/viewer/src/icons/dark-mode.svg +4 -0
  36. iXBRLViewerPlugin/viewer/src/img/arelle-dark.svg +179 -0
  37. iXBRLViewerPlugin/viewer/src/img/inline-viewer-dark.svg +59 -0
  38. iXBRLViewerPlugin/viewer/src/js/accordian.js +3 -2
  39. iXBRLViewerPlugin/viewer/src/js/aspect.js +18 -10
  40. iXBRLViewerPlugin/viewer/src/js/aspect.test.js +2 -2
  41. iXBRLViewerPlugin/viewer/src/js/balance.js +14 -0
  42. iXBRLViewerPlugin/viewer/src/js/calculation.js +45 -34
  43. iXBRLViewerPlugin/viewer/src/js/calculationInspector.js +4 -7
  44. iXBRLViewerPlugin/viewer/src/js/chart.js +23 -21
  45. iXBRLViewerPlugin/viewer/src/js/concept.js +27 -2
  46. iXBRLViewerPlugin/viewer/src/js/concept.test.js +23 -2
  47. iXBRLViewerPlugin/viewer/src/js/datatype.js +20 -0
  48. iXBRLViewerPlugin/viewer/src/js/datatype.test.js +62 -0
  49. iXBRLViewerPlugin/viewer/src/js/dialog.js +6 -4
  50. iXBRLViewerPlugin/viewer/src/js/fact.js +41 -8
  51. iXBRLViewerPlugin/viewer/src/js/fact.test.js +47 -13
  52. iXBRLViewerPlugin/viewer/src/js/index.js +11 -3
  53. iXBRLViewerPlugin/viewer/src/js/inspector.js +560 -160
  54. iXBRLViewerPlugin/viewer/src/js/inspector.test.js +1 -2
  55. iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.js +129 -31
  56. iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.test.js +133 -20
  57. iXBRLViewerPlugin/viewer/src/js/ixnode.js +1 -1
  58. iXBRLViewerPlugin/viewer/src/js/menu.js +25 -7
  59. iXBRLViewerPlugin/viewer/src/js/number-matcher.js +2 -2
  60. iXBRLViewerPlugin/viewer/src/js/outline.js +2 -4
  61. iXBRLViewerPlugin/viewer/src/js/period.js +0 -1
  62. iXBRLViewerPlugin/viewer/src/js/report.js +68 -13
  63. iXBRLViewerPlugin/viewer/src/js/report.test.js +77 -6
  64. iXBRLViewerPlugin/viewer/src/js/reportset.js +33 -3
  65. iXBRLViewerPlugin/viewer/src/js/reportset.test.js +32 -6
  66. iXBRLViewerPlugin/viewer/src/js/search.js +61 -35
  67. iXBRLViewerPlugin/viewer/src/js/search.test.js +8 -5
  68. iXBRLViewerPlugin/viewer/src/js/summary.js +22 -0
  69. iXBRLViewerPlugin/viewer/src/js/summary.test.js +50 -13
  70. iXBRLViewerPlugin/viewer/src/js/tableExport.js +3 -3
  71. iXBRLViewerPlugin/viewer/src/js/taxonomynamer.js +34 -0
  72. iXBRLViewerPlugin/viewer/src/js/taxonomynamer.test.js +32 -0
  73. iXBRLViewerPlugin/viewer/src/js/theme.js +49 -0
  74. iXBRLViewerPlugin/viewer/src/js/unit.js +73 -2
  75. iXBRLViewerPlugin/viewer/src/js/unit.test.js +14 -3
  76. iXBRLViewerPlugin/viewer/src/js/util.js +21 -18
  77. iXBRLViewerPlugin/viewer/src/js/util.test.js +1 -0
  78. iXBRLViewerPlugin/viewer/src/js/utr.js +27 -0
  79. iXBRLViewerPlugin/viewer/src/js/viewer.js +40 -29
  80. iXBRLViewerPlugin/viewer/src/js/viewerOptions.js +0 -2
  81. iXBRLViewerPlugin/viewer/src/less/accordian.less +8 -4
  82. iXBRLViewerPlugin/viewer/src/less/block-list.less +12 -6
  83. iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +2 -2
  84. iXBRLViewerPlugin/viewer/src/less/chart.less +8 -5
  85. iXBRLViewerPlugin/viewer/src/less/colours-dark-mode.less +40 -0
  86. iXBRLViewerPlugin/viewer/src/less/colours.less +28 -21
  87. iXBRLViewerPlugin/viewer/src/less/common.less +1 -1
  88. iXBRLViewerPlugin/viewer/src/less/components.less +3 -3
  89. iXBRLViewerPlugin/viewer/src/less/core.less +2 -0
  90. iXBRLViewerPlugin/viewer/src/less/dialog.less +13 -10
  91. iXBRLViewerPlugin/viewer/src/less/form-controls.less +33 -11
  92. iXBRLViewerPlugin/viewer/src/less/inspector.less +556 -300
  93. iXBRLViewerPlugin/viewer/src/less/loader.less +2 -2
  94. iXBRLViewerPlugin/viewer/src/less/menu.less +33 -15
  95. iXBRLViewerPlugin/viewer/src/less/summary.less +16 -6
  96. iXBRLViewerPlugin/viewer/src/less/tabs.less +5 -5
  97. iXBRLViewerPlugin/viewer/src/less/text-mixins.less +2 -1
  98. iXBRLViewerPlugin/viewer/src/less/validation-report.less +1 -1
  99. iXBRLViewerPlugin/viewer/src/less/viewer.less +30 -18
  100. iXBRLViewerPlugin/viewer/webpack.common.js +19 -9
  101. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/METADATA +41 -14
  102. ixbrl_viewer-1.4.49.dist-info/RECORD +197 -0
  103. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/WHEEL +1 -1
  104. tests/puppeteer/framework/page_objects/doc_frame.js +3 -3
  105. tests/puppeteer/framework/page_objects/fact_details_panel.js +28 -0
  106. tests/puppeteer/puppeteer_test_run_via_intellij.jpg +0 -0
  107. tests/puppeteer/tests/fact_properties.test.js +10 -4
  108. tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +117 -51
  109. iXBRLViewerPlugin/viewer/src/js/interact.min.js +0 -6
  110. ixbrl_viewer-1.4.20.dist-info/RECORD +0 -166
  111. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/LICENSE +0 -0
  112. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/NOTICE +0 -0
  113. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/entry_points.txt +0 -0
  114. {ixbrl_viewer-1.4.20.dist-info → ixbrl_viewer-1.4.49.dist-info}/top_level.txt +0 -0
@@ -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, reports: list[ModelXbrl], basenameSuffix: str = ''):
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
- def enableFeature(self, featureName: str):
99
- if featureName in self.taxonomyData["features"]:
100
- return
101
- featureNames = [c.key for c in FEATURE_CONFIGS]
102
- assert featureName in featureNames, \
103
- f'Given feature name `{featureName}` does not match any defined features: {featureNames}'
104
- self.taxonomyData["features"].append(featureName)
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 addELR(self, report: ModelXbrl, elr):
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.type is not None and concept.type.isTextBlock:
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.addELR(report, ELR)
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.logger_model.modelManager.cntlr.logHandler
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.logger_model.error(ERROR_MESSAGE_CODE, "File already contains iXBRL viewer")
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 len(self.reports) == 1 and os.path.dirname(self.reports[0].modelDocument.filepath).endswith('.zip'):
484
- filingDocZipPath = os.path.dirname(self.reports[0].modelDocument.filepath)
485
- filingDocZipName = os.path.basename(filingDocZipPath)
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
- return iv
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, logger_model: ModelXbrl):
562
+ def __init__(self, cntlr: Cntlr):
563
+ self.reportZip = None
505
564
  self.files = []
506
565
  self.filingDocuments = None
507
- # This is an arbitrary ModelXbrl used for logging only
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
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.logger_model.error(ERROR_MESSAGE_CODE, "File extension %s is not a zip" % os.path.splitext(destination)[0])
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.logger_model.info(INFO_MESSAGE_CODE, "Saving in output zip %s" % f.filename)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, f"Writing script from {copyScriptPath}")
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.error(ERROR_MESSAGE_CODE, "More than one file in input, but output is not a directory")
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % destination)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, f"Copying script from {scriptDest} to {scriptDest}.")
676
+ self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
606
677
  shutil.copy2(scriptPath, scriptDest)
@@ -0,0 +1,7 @@
1
+ from arelle.utils.PluginData import PluginData
2
+ from .iXBRLViewer import IXBRLViewerBuilder
3
+ from typing import Optional
4
+
5
+ class IXBRLViewerPluginData(PluginData): # type: ignore[misc]
6
+
7
+ builder: Optional[IXBRLViewerBuilder] = None