arelle-release 2.37.60__py3-none-any.whl → 2.37.62__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 arelle-release might be problematic. Click here for more details.

Files changed (37) hide show
  1. arelle/DisclosureSystem.py +5 -0
  2. arelle/HtmlUtil.py +5 -4
  3. arelle/ModelDtsObject.py +6 -0
  4. arelle/ValidateDuplicateFacts.py +2 -0
  5. arelle/ValidateXbrl.py +3 -1
  6. arelle/ValidateXbrlDTS.py +1 -1
  7. arelle/XbrlConst.py +18 -0
  8. arelle/_version.py +2 -2
  9. arelle/config/disclosuresystems.xsd +1 -0
  10. arelle/plugin/validate/EDINET/Constants.py +95 -0
  11. arelle/plugin/validate/EDINET/ControllerPluginData.py +14 -3
  12. arelle/plugin/validate/EDINET/CoverItemRequirements.py +42 -0
  13. arelle/plugin/validate/EDINET/{CoverPageRequirements.py → DeiRequirements.py} +24 -24
  14. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +209 -43
  15. arelle/plugin/validate/EDINET/ReportFolderType.py +61 -0
  16. arelle/plugin/validate/EDINET/TableOfContentsBuilder.py +493 -0
  17. arelle/plugin/validate/EDINET/__init__.py +13 -2
  18. arelle/plugin/validate/EDINET/resources/config.xml +6 -0
  19. arelle/plugin/validate/EDINET/resources/cover-item-requirements.json +793 -0
  20. arelle/plugin/validate/EDINET/resources/edinet-taxonomies.xml +2 -0
  21. arelle/plugin/validate/EDINET/rules/contexts.py +61 -1
  22. arelle/plugin/validate/EDINET/rules/edinet.py +278 -4
  23. arelle/plugin/validate/EDINET/rules/frta.py +122 -3
  24. arelle/plugin/validate/EDINET/rules/gfm.py +849 -5
  25. arelle/plugin/validate/EDINET/rules/upload.py +231 -192
  26. arelle/plugin/validate/NL/PluginValidationDataExtension.py +6 -8
  27. arelle/plugin/validate/NL/ValidationPluginExtension.py +0 -3
  28. arelle/plugin/validate/NL/rules/nl_kvk.py +1 -2
  29. arelle/utils/validate/ValidationPlugin.py +1 -1
  30. arelle/utils/validate/ValidationUtil.py +1 -2
  31. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/METADATA +2 -1
  32. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/RECORD +37 -34
  33. /arelle/plugin/validate/EDINET/resources/{cover-page-requirements.csv → dei-requirements.csv} +0 -0
  34. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/WHEEL +0 -0
  35. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/entry_points.txt +0 -0
  36. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/licenses/LICENSE.md +0 -0
  37. {arelle_release-2.37.60.dist-info → arelle_release-2.37.62.dist-info}/top_level.txt +0 -0
@@ -16,8 +16,9 @@ from typing import Callable, Hashable, Iterable, cast
16
16
  import os
17
17
  import regex
18
18
 
19
+ from arelle import XbrlConst
19
20
  from arelle.LinkbaseType import LinkbaseType
20
- from arelle.ModelDocument import Type as ModelDocumentType, ModelDocument
21
+ from arelle.ModelDocument import Type as ModelDocumentType, ModelDocument, load as modelDocumentLoad
21
22
  from arelle.ModelDtsObject import ModelConcept
22
23
  from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext, ModelInlineFact
23
24
  from arelle.ModelObject import ModelObject
@@ -32,12 +33,14 @@ from arelle.typing import TypeGetText
32
33
  from arelle.utils.PluginData import PluginData
33
34
  from .Constants import xhtmlDtdExtension, PROHIBITED_HTML_TAGS, PROHIBITED_HTML_ATTRIBUTES
34
35
  from .ControllerPluginData import ControllerPluginData
35
- from .CoverPageRequirements import CoverPageRequirements, COVER_PAGE_ITEM_LOCAL_NAMES
36
+ from .CoverItemRequirements import CoverItemRequirements
37
+ from .DeiRequirements import DeiRequirements, DEI_LOCAL_NAMES
36
38
  from .FilingFormat import FilingFormat, FILING_FORMATS
37
39
  from .FormType import FormType
38
40
  from .ManifestInstance import ManifestInstance
41
+ from .ReportFolderType import HTML_EXTENSIONS
39
42
  from .Statement import Statement, STATEMENTS, BalanceSheet, StatementInstance, StatementType
40
- from .UploadContents import UploadContents
43
+ from .UploadContents import UploadContents, UploadPathInfo
41
44
 
42
45
  _: TypeGetText
43
46
 
@@ -57,6 +60,7 @@ STANDARD_TAXONOMY_URL_PREFIXES = frozenset((
57
60
  'https://www.w3.org/1999/xlink'
58
61
  ))
59
62
 
63
+
60
64
  @dataclass(frozen=True)
61
65
  class UriReference:
62
66
  attributeName: str
@@ -67,27 +71,41 @@ class UriReference:
67
71
 
68
72
  @dataclass
69
73
  class PluginValidationDataExtension(PluginData):
74
+ # Namespaces
75
+ jpcrpEsrNamespace: str
76
+ jpcrpNamespace: str
77
+ jpctlNamespace: str
78
+ jpcrpSbrNamespace: str
79
+ jpdeiNamespace: str
80
+ jpigpNamespace: str
81
+ jplvhNamespace: str
82
+ jppfsNamespace: str
83
+ jpspsEsrNamespace: str
84
+ jpspsSbrNamespace: str
85
+ jpspsNamespace: str
86
+ jptoiNamespace: str
87
+ jptooPstNamespace: str
88
+ jptooToaNamespace: str
89
+ jptooTonNamespace: str
90
+ jptooTorNamespace: str
91
+ jptooWtoNamespace: str
92
+
70
93
  accountingStandardsDeiQn: QName
71
94
  assetsIfrsQn: QName
72
95
  consolidatedOrNonConsolidatedAxisQn: QName
73
96
  documentTypeDeiQn: QName
74
97
  jpcrpEsrFilingDateCoverPageQn: QName
75
- jpcrpEsrNamespace: str
76
98
  jpcrpFilingDateCoverPageQn: QName
77
- jpcrpNamespace: str
78
- jpdeiNamespace: str
79
- jpigpNamespace: str
80
- jppfsNamespace: str
81
99
  jpspsFilingDateCoverPageQn: QName
82
- jpspsNamespace: str
83
100
  nonConsolidatedMemberQn: QName
84
101
  ratioOfFemaleDirectorsAndOtherOfficersQn: QName
85
102
 
86
- contextIdPattern: regex.Pattern[str]
87
- coverPageItems: tuple[QName, ...]
88
- coverPageRequirementsPath: Path
103
+ coverItemRequirementsPath: Path
89
104
  coverPageTitleQns: tuple[QName, ...]
105
+ deiItems: tuple[QName, ...]
106
+ deiRequirementsPath: Path
90
107
 
108
+ _namespaceMap: dict[str, str]
91
109
  _uriReferences: list[UriReference]
92
110
 
93
111
  def __init__(self, name: str, validateXbrl: ValidateXbrl):
@@ -96,10 +114,40 @@ class PluginValidationDataExtension(PluginData):
96
114
  # Namespaces
97
115
  self.jpcrpEsrNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp-esr/2024-11-01/jpcrp-esr_cor"
98
116
  self.jpcrpNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2024-11-01/jpcrp_cor'
117
+ self.jpcrpSbrNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp-sbr/2024-11-01/jpcrp-sbr_cor"
118
+ self.jpctlNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpctl/2024-11-01/jpctl_cor'
99
119
  self.jpdeiNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpdei/2013-08-31/jpdei_cor'
100
120
  self.jpigpNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpigp/2024-11-01/jpigp_cor"
121
+ self.jplvhNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jplvh/2024-11-01/jplvh_cor"
101
122
  self.jppfsNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jppfs/2024-11-01/jppfs_cor"
123
+ self.jpspsEsrNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpsps-esr/2024-11-01/jpsps-esr_cor'
124
+ self.jpspsSbrNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpsps-sbr/2024-11-01/jpsps-sbr_cor'
102
125
  self.jpspsNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpsps/2024-11-01/jpsps_cor'
126
+ self.jptoiNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoi/2024-11-01/jptoi_cor'
127
+ self.jptooPstNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoo-pst/2024-11-01/jptoo-pst_cor'
128
+ self.jptooToaNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoo-toa/2024-11-01/jptoo-toa_cor'
129
+ self.jptooTonNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoo-ton/2024-11-01/jptoo-ton_cor'
130
+ self.jptooTorNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoo-tor/2024-11-01/jptoo-tor_cor'
131
+ self.jptooWtoNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jptoo-wto/2024-11-01/jptoo-wto_cor'
132
+ self._namespaceMap = {
133
+ "jpcrp-esr_cor": self.jpcrpEsrNamespace,
134
+ "jpcrp-sbr_cor": self.jpcrpSbrNamespace,
135
+ "jpcrp_cor": self.jpcrpNamespace,
136
+ "jpctl_cor": self.jpctlNamespace,
137
+ "jpdei_cor": self.jpdeiNamespace,
138
+ "jpigp_cor": self.jpigpNamespace,
139
+ "jplvh_cor": self.jplvhNamespace,
140
+ "jppfs_cor": self.jppfsNamespace,
141
+ "jpsps_cor": self.jpspsNamespace,
142
+ "jpsps-esr_cor": self.jpspsEsrNamespace,
143
+ "jpsps-sbr_cor": self.jpspsSbrNamespace,
144
+ "jptoi_cor": self.jptoiNamespace,
145
+ "jptoo-pst_cor": self.jptooPstNamespace,
146
+ "jptoo-toa_cor": self.jptooToaNamespace,
147
+ "jptoo-ton_cor": self.jptooTonNamespace,
148
+ "jptoo-tor_cor": self.jptooPstNamespace,
149
+ "jptoo-wto_cor": self.jptooWtoNamespace,
150
+ }
103
151
 
104
152
  # QNames
105
153
  self.accountingStandardsDeiQn = qname(self.jpdeiNamespace, 'AccountingStandardsDEI')
@@ -113,18 +161,18 @@ class PluginValidationDataExtension(PluginData):
113
161
  self.nonConsolidatedMemberQn = qname(self.jppfsNamespace, "NonConsolidatedMember")
114
162
  self.ratioOfFemaleDirectorsAndOtherOfficersQn = qname(self.jpcrpNamespace, "RatioOfFemaleDirectorsAndOtherOfficers")
115
163
 
116
- self.contextIdPattern = regex.compile(r'(Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Interim)(Duration|Instant)')
117
- self.coverPageItems = tuple(
118
- qname(self.jpdeiNamespace, localName)
119
- for localName in COVER_PAGE_ITEM_LOCAL_NAMES
120
- )
121
- self.coverPageRequirementsPath = Path(__file__).parent / "resources" / "cover-page-requirements.csv"
164
+ self.coverItemRequirementsPath = Path(__file__).parent / "resources" / "cover-item-requirements.json"
122
165
  self.coverPageTitleQns = (
123
166
  qname(self.jpspsNamespace, "DocumentTitleAnnualSecuritiesReportCoverPage"),
124
167
  qname(self.jpcrpNamespace, "DocumentTitleCoverPage"),
125
168
  qname(self.jpcrpEsrNamespace, "DocumentTitleCoverPage"),
126
169
  qname(self.jpspsNamespace, "DocumentTitleCoverPage"),
127
170
  )
171
+ self.deiItems = tuple(
172
+ qname(self.jpdeiNamespace, localName)
173
+ for localName in DEI_LOCAL_NAMES
174
+ )
175
+ self.deiRequirementsPath = Path(__file__).parent / "resources" / "dei-requirements.csv"
128
176
 
129
177
  self._uriReferences = []
130
178
  self._initialize(validateXbrl.modelXbrl)
@@ -148,27 +196,62 @@ class PluginValidationDataExtension(PluginData):
148
196
  contextIsConsolidated = memberValue != self.nonConsolidatedMemberQn
149
197
  return bool(statement.isConsolidated == contextIsConsolidated)
150
198
 
199
+ def _initializeDocument(self, uri: str, modelDocument: ModelDocument, modelXbrl: ModelXbrl) -> None:
200
+ docPath = Path(uri)
201
+ basePath = Path(str(modelXbrl.fileSource.basefile))
202
+ if not docPath.is_relative_to(basePath):
203
+ return
204
+ controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
205
+ controllerPluginData.addUsedFilepath(docPath.relative_to(basePath))
206
+ for elt, name, value in self.getUriAttributeValues(modelDocument):
207
+ self._uriReferences.append(UriReference(
208
+ attributeName=name,
209
+ attributeValue=value,
210
+ document=modelDocument,
211
+ element=elt,
212
+ ))
213
+ fullPath = (Path(modelDocument.uri).parent / value).resolve()
214
+ if fullPath.is_relative_to(basePath):
215
+ fileSourcePath = fullPath.relative_to(basePath)
216
+ controllerPluginData.addUsedFilepath(fileSourcePath)
217
+ referenceUri = str(fullPath)
218
+ if (
219
+ fullPath.suffix in HTML_EXTENSIONS and
220
+ referenceUri not in modelXbrl.urlDocs and
221
+ modelXbrl.fileSource.exists(referenceUri)
222
+ ):
223
+ referenceModelDocument = modelDocumentLoad(
224
+ modelXbrl,
225
+ referenceUri,
226
+ referringElement=elt
227
+ )
228
+ if referenceModelDocument is not None:
229
+ self._initializeDocument(referenceUri, referenceModelDocument, modelXbrl)
230
+
151
231
  def _initialize(self, modelXbrl: ModelXbrl) -> None:
152
232
  if not isinstance(modelXbrl.fileSource.basefile, str):
153
233
  return
234
+ # Additional documents may be loaded, so make a copy to iterate over.
235
+ urlDocs = list(modelXbrl.urlDocs.items())
236
+ for uri, modelDocument in urlDocs:
237
+ self._initializeDocument(uri, modelDocument, modelXbrl)
238
+
239
+ def addToTableOfContents(self, modelXbrl: ModelXbrl) -> None:
240
+ uploadContents = self.getUploadContents(modelXbrl)
241
+ if uploadContents is None:
242
+ return
154
243
  controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
155
- basePath = Path(modelXbrl.fileSource.basefile)
156
- for uri, doc in modelXbrl.urlDocs.items():
157
- docPath = Path(uri)
158
- if not docPath.is_relative_to(basePath):
244
+ tocBuilder = controllerPluginData.getTableOfContentsBuilder()
245
+ for modelDocument in modelXbrl.urlDocs.values():
246
+ path = Path(modelDocument.uri)
247
+ if modelDocument.type != ModelDocumentType.INLINEXBRL:
159
248
  continue
160
- controllerPluginData.addUsedFilepath(docPath.relative_to(basePath))
161
- for elt, name, value in self.getUriAttributeValues(doc):
162
- self._uriReferences.append(UriReference(
163
- attributeName=name,
164
- attributeValue=value,
165
- document=doc,
166
- element=elt,
167
- ))
168
- fullPath = Path(doc.uri).parent / value
169
- if fullPath.is_relative_to(basePath):
170
- fileSourcePath = fullPath.relative_to(basePath)
171
- controllerPluginData.addUsedFilepath(fileSourcePath)
249
+ pathInfo = uploadContents.uploadPathsByFullPath.get(path)
250
+ if pathInfo is not None and not pathInfo.isCoverPage:
251
+ tocBuilder.addDocument(modelDocument)
252
+
253
+ def qname(self, prefix: str, localName: str) -> QName:
254
+ return qname(self._namespaceMap[prefix], localName)
172
255
 
173
256
  @lru_cache(1)
174
257
  def isCorporateForm(self, modelXbrl: ModelXbrl) -> bool:
@@ -202,10 +285,22 @@ class PluginValidationDataExtension(PluginData):
202
285
  extensionConcepts = []
203
286
  for concepts in modelXbrl.nameConcepts.values():
204
287
  for concept in concepts:
205
- if self.isExtensionUri(concept.qname.namespaceURI, modelXbrl):
288
+ if self.isExtensionUri(concept.document.uri, modelXbrl):
206
289
  extensionConcepts.append(concept)
207
290
  return extensionConcepts
208
291
 
292
+ @lru_cache(1)
293
+ def getUsedConcepts(self, modelXbrl: ModelXbrl) -> set[ModelConcept]:
294
+ """
295
+ Returns a set of concepts used on facts and in explicit dimensions
296
+ """
297
+ usedConcepts = {fact.concept for fact in modelXbrl.facts if fact.concept is not None}
298
+ for context in modelXbrl.contextsInUse:
299
+ for dim in context.scenDimValues.values():
300
+ if dim.isExplicit:
301
+ usedConcepts.update([dim.dimension, dim.member])
302
+ return usedConcepts
303
+
209
304
  def getBalanceSheets(self, modelXbrl: ModelXbrl, statement: Statement) -> list[BalanceSheet]:
210
305
  """
211
306
  :return: Balance sheet data for each context/unit pairing the given statement.
@@ -261,9 +356,60 @@ class PluginValidationDataExtension(PluginData):
261
356
  )
262
357
  return balanceSheets
263
358
 
264
- def getCoverPageRequirements(self, modelXbrl: ModelXbrl) -> CoverPageRequirements:
359
+ @lru_cache(1)
360
+ def getCoverItemRequirements(self, modelXbrl: ModelXbrl) -> list[QName] | None:
361
+ manifestInstance = self.getManifestInstance(modelXbrl)
362
+ if manifestInstance is None:
363
+ return None
364
+ if any(e is not None and e.startswith('EDINET.EC5800E') for e in modelXbrl.errors):
365
+ # Manifest TOC parsing failed, so cannot determine cover items.
366
+ return None
367
+ assert len(manifestInstance.tocItems) == 1, _("Only one TOC item should be associated with this instance.")
368
+ roleUri = manifestInstance.tocItems[0].extrole
369
+ roleUri = roleUri.replace("_std_", "_")
370
+ controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
371
+ coverItemRequirements = controllerPluginData.getCoverItemRequirements(self.coverItemRequirementsPath)
372
+ coverItems = coverItemRequirements.get(roleUri)
373
+ return [
374
+ self.qname(prefix, localName)
375
+ for prefix, localName in
376
+ [name.split(':') for name in coverItems]
377
+ ]
378
+
379
+ @lru_cache(1)
380
+ def getCoverItems(self, modelXbrl: ModelXbrl) -> frozenset[QName]:
381
+ controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
382
+ coverItemRequirements = controllerPluginData.getCoverItemRequirements(self.coverItemRequirementsPath)
383
+ coverItems = coverItemRequirements.all()
384
+ return frozenset(
385
+ self.qname(prefix, localName)
386
+ for prefix, localName in
387
+ [name.split(':') for name in coverItems]
388
+ )
389
+
390
+ def getDeiRequirements(self, modelXbrl: ModelXbrl) -> DeiRequirements:
265
391
  controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
266
- return controllerPluginData.getCoverPageRequirements(self.coverPageRequirementsPath, self.coverPageItems, FILING_FORMATS)
392
+ return controllerPluginData.getDeiRequirements(self.deiRequirementsPath, self.deiItems, FILING_FORMATS)
393
+
394
+ @lru_cache(1)
395
+ def getExtensionSchemas(self, modelXbrl: ModelXbrl) -> dict[str, UploadPathInfo]:
396
+ namespacePathInfos: dict[str, UploadPathInfo] = {}
397
+ uploadContents = self.getUploadContents(modelXbrl)
398
+ if uploadContents is None:
399
+ return namespacePathInfos
400
+ for modelDocument in modelXbrl.urlDocs.values():
401
+ if modelDocument.type != ModelDocumentType.SCHEMA:
402
+ continue # Not a schema
403
+ if modelDocument.targetNamespace is None:
404
+ continue # No target namespace
405
+ if not self.isExtensionUri(modelDocument.uri, modelXbrl):
406
+ continue # Not an extension schema
407
+ path = Path(modelDocument.uri)
408
+ pathInfo = uploadContents.uploadPathsByFullPath.get(path)
409
+ if pathInfo is None or pathInfo.reportFolderType is None:
410
+ continue # Not part of the filing, error will be caught elsewhere
411
+ namespacePathInfos[modelDocument.targetNamespace] = pathInfo
412
+ return namespacePathInfos
267
413
 
268
414
  def getProblematicTextBlocks(self, modelXbrl: ModelXbrl) -> list[ModelInlineFact]:
269
415
  problematicTextBlocks: list[ModelInlineFact] = []
@@ -356,10 +502,9 @@ class PluginValidationDataExtension(PluginData):
356
502
  # values assigned to the various FilingFormats. This may only be by coincidence or convention.
357
503
  # If it doesn't end up being reliable, we may need to find another way to identify the form.
358
504
  # For example, by disclosure system selection or CLI argument.
359
- documentTitleFacts = []
505
+ documentTitleFacts: list[ModelFact] = []
360
506
  for qname in self.coverPageTitleQns:
361
- for fact in self.iterValidNonNilFacts(modelXbrl, qname):
362
- documentTitleFacts.append(fact)
507
+ documentTitleFacts.extend(self.iterValidNonNilFacts(modelXbrl, qname))
363
508
  formTypes = self.getFormTypes(modelXbrl)
364
509
  filingFormats = []
365
510
  for filingFormatIndex, filingFormat in enumerate(FILING_FORMATS):
@@ -367,7 +512,7 @@ class PluginValidationDataExtension(PluginData):
367
512
  continue
368
513
  prefixes = {taxonomy.value for taxonomy in filingFormat.taxonomies}
369
514
  if not any(
370
- str(fact.xValue).startswith(filingFormat.documentType.value) and
515
+ str(fact.xValue).strip().startswith(filingFormat.documentType.value) and
371
516
  fact.concept.qname.prefix.split('_')[0] in prefixes
372
517
  for fact in documentTitleFacts
373
518
  ):
@@ -378,7 +523,7 @@ class PluginValidationDataExtension(PluginData):
378
523
  "arelle:NoMatchingEdinetFormat",
379
524
  _("No matching EDINET filing formats could be identified based on form "
380
525
  "type (%(formTypes)s) and title."),
381
- formTypes=formTypes,
526
+ formTypes=", ".join(t.value for t in formTypes),
382
527
  modelObject=documentTitleFacts,
383
528
  )
384
529
  return None
@@ -451,6 +596,20 @@ class PluginValidationDataExtension(PluginData):
451
596
  elts.append(elt)
452
597
  return elts
453
598
 
599
+ def getStandardTaxonomyExtensionLinks(self, linkbaseType: LinkbaseType, modelXbrl: ModelXbrl) -> list[ModelObject]:
600
+ elts: list[ModelObject] = []
601
+ for modelDocument in modelXbrl.urlDocs.values():
602
+ if self.isStandardTaxonomyUrl(modelDocument.uri, modelXbrl) or not modelDocument.type == ModelDocumentType.SCHEMA:
603
+ continue
604
+ rootElt = modelDocument.xmlRootElement
605
+ for elt in rootElt.iterdescendants(XbrlConst.qnLinkLinkbaseRef.clarkNotation):
606
+ uri = elt.attrib.get(XbrlConst.qnXlinkHref.clarkNotation)
607
+ role = elt.attrib.get(XbrlConst.qnXlinkRole.clarkNotation)
608
+ if not role == linkbaseType.getRefUri() or self.isExtensionUri(uri, modelXbrl):
609
+ continue
610
+ elts.append(elt)
611
+ return elts
612
+
454
613
  def getUploadContents(self, modelXbrl: ModelXbrl) -> UploadContents | None:
455
614
  controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
456
615
  return controllerPluginData.getUploadContents()
@@ -458,9 +617,16 @@ class PluginValidationDataExtension(PluginData):
458
617
  @lru_cache(1)
459
618
  def getUriAttributeValues(self, modelDocument: ModelDocument) -> list[tuple[ModelObject, str, str]]:
460
619
  results: list[tuple[ModelObject, str, str]] = []
461
- if modelDocument.type not in (ModelDocumentType.INLINEXBRL, ModelDocumentType.HTML):
620
+ modelDocumentType = modelDocument.type
621
+ # Normal document parsing does not assign the HTML type to HTML files.
622
+ # Use ModelDocumentType.identify to check for HTML files.
623
+ if modelDocumentType == ModelDocumentType.UnknownXML:
624
+ modelDocumentType = ModelDocumentType.identify(modelDocument.modelXbrl.fileSource, modelDocument.uri)
625
+ if modelDocumentType not in (ModelDocumentType.INLINEXBRL, ModelDocumentType.HTML):
462
626
  return results
463
627
  for elt in modelDocument.xmlRootElement.iter():
628
+ if not isinstance(elt, ModelObject):
629
+ continue
464
630
  for name in htmlEltUriAttrs.get(elt.localName, ()):
465
631
  value = elt.get(name)
466
632
  if value is not None:
@@ -7,6 +7,10 @@ from enum import Enum
7
7
  from functools import cached_property, lru_cache
8
8
  from pathlib import Path
9
9
 
10
+ from regex import Pattern
11
+
12
+ from . import Constants
13
+
10
14
 
11
15
  class ReportFolderType(Enum):
12
16
  ATTACH_DOC = "AttachDoc"
@@ -30,6 +34,10 @@ class ReportFolderType(Enum):
30
34
  def isAttachment(self) -> bool:
31
35
  return "Attach" in self.value
32
36
 
37
+ @cached_property
38
+ def ixbrlFilenamePatterns(self) -> list[Pattern[str]]:
39
+ return IXBRL_FILENAME_PATTERNS.get(self, [])
40
+
33
41
  @cached_property
34
42
  def manifestName(self) -> str:
35
43
  return f'manifest_{self.value}.xml'
@@ -38,10 +46,22 @@ class ReportFolderType(Enum):
38
46
  def manifestPath(self) -> Path:
39
47
  return self.xbrlDirectory / self.manifestName
40
48
 
49
+ @cached_property
50
+ def namespaceUriPatterns(self) -> list[Pattern[str]]:
51
+ return NAMESPACE_URI_PATTERNS.get(self, [])
52
+
53
+ @cached_property
54
+ def prefixPatterns(self) -> list[Pattern[str]]:
55
+ return PREFIX_PATTERNS.get(self, [])
56
+
41
57
  @cached_property
42
58
  def xbrlDirectory(self) -> Path:
43
59
  return Path('XBRL') / str(self.value)
44
60
 
61
+ @cached_property
62
+ def xbrlFilenamePatterns(self) -> list[Pattern[str]]:
63
+ return XBRL_FILENAME_PATTERNS.get(self, [])
64
+
45
65
  @lru_cache(1)
46
66
  def getValidExtensions(self, isAmendment: bool, isSubdirectory: bool) -> frozenset[str] | None:
47
67
  if self.extensionCategory is None:
@@ -99,3 +119,44 @@ VALID_EXTENSIONS = {
99
119
  },
100
120
  },
101
121
  }
122
+
123
+ IXBRL_FILENAME_PATTERNS = {
124
+ ReportFolderType.AUDIT_DOC: [
125
+ Constants.AUDIT_IXBRL_FILENAME_PATTERN,
126
+ Constants.AUDIT_LINKBASE_FILENAME_PATTERN,
127
+ Constants.AUDIT_SCHEMA_FILENAME_PATTERN,
128
+ ],
129
+ ReportFolderType.PUBLIC_DOC: [
130
+ Constants.REPORT_COVER_FILENAME_PATTERN,
131
+ Constants.REPORT_IXBRL_FILENAME_PATTERN,
132
+ Constants.REPORT_LINKBASE_FILENAME_PATTERN,
133
+ Constants.REPORT_SCHEMA_FILENAME_PATTERN,
134
+ ]
135
+ }
136
+
137
+ NAMESPACE_URI_PATTERNS = {
138
+ ReportFolderType.AUDIT_DOC: [
139
+ Constants.AUDIT_NAMESPACE_URI_PATTERN,
140
+ ],
141
+ ReportFolderType.PUBLIC_DOC: [
142
+ Constants.REPORT_NAMESPACE_URI_PATTERN,
143
+ ],
144
+ }
145
+
146
+ PREFIX_PATTERNS = {
147
+ ReportFolderType.AUDIT_DOC: [
148
+ Constants.AUDIT_PREFIX_PATTERN,
149
+ ],
150
+ ReportFolderType.PUBLIC_DOC: [
151
+ Constants.REPORT_PREFIX_PATTERN,
152
+ ],
153
+ }
154
+
155
+ XBRL_FILENAME_PATTERNS = {
156
+ ReportFolderType.AUDIT_DOC: [
157
+ Constants.AUDIT_XBRL_FILENAME_PATTERN,
158
+ ],
159
+ ReportFolderType.PUBLIC_DOC: [
160
+ Constants.REPORT_XBRL_FILENAME_PATTERN,
161
+ ],
162
+ }