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.

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 +210 -137
  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 +30 -3
  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 +40 -7
  51. iXBRLViewerPlugin/viewer/src/js/fact.test.js +3 -0
  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 +28 -2
  69. iXBRLViewerPlugin/viewer/src/js/summary.test.js +52 -14
  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.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/METADATA +41 -14
  102. ixbrl_viewer-1.4.50.dist-info/RECORD +197 -0
  103. {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.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 +29 -1
  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.21.dist-info/RECORD +0 -166
  111. {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/LICENSE +0 -0
  112. {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/NOTICE +0 -0
  113. {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.dist-info}/entry_points.txt +0 -0
  114. {ixbrl_viewer-1.4.21.dist-info → ixbrl_viewer-1.4.50.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,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 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):
505
- self.files = []
562
+ def __init__(self, cntlr: Cntlr):
563
+ self.reportZip = None
564
+ self.filesByFilename = dict()
506
565
  self.filingDocuments = None
507
- # This is an arbitrary ModelXbrl used for logging only
508
- self.logger_model = logger_model
509
- self.filenames = set()
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
- if ivf.filename in self.filenames:
513
- return
514
- self.files.append(ivf)
515
- self.filenames.add(ivf.filename)
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
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.logger_model.error(ERROR_MESSAGE_CODE, "File extension %s is not a zip" % os.path.splitext(destination)[0])
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.logger_model.info(INFO_MESSAGE_CODE, "Saving in output zip %s" % f.filename)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, f"Writing script from {copyScriptPath}")
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.error(ERROR_MESSAGE_CODE, "More than one file in input, but output is not a directory")
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
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.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % destination)
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.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
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.logger_model.info(INFO_MESSAGE_CODE, f"Copying script from {scriptDest} to {scriptDest}.")
678
+ self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
606
679
  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