arelle-release 2.37.13__py3-none-any.whl → 2.37.15__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 (26) hide show
  1. arelle/CntlrCmdLine.py +1 -1
  2. arelle/ModelTestcaseObject.py +7 -0
  3. arelle/Validate.py +14 -2
  4. arelle/ValidateDuplicateFacts.py +4 -10
  5. arelle/_version.py +2 -2
  6. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +9 -1
  7. arelle/plugin/validate/DBA/ValidationPluginExtension.py +4 -2
  8. arelle/plugin/validate/DBA/rules/fr.py +30 -32
  9. arelle/plugin/validate/DBA/rules/tm.py +22 -12
  10. arelle/plugin/validate/NL/PluginValidationDataExtension.py +154 -26
  11. arelle/plugin/validate/NL/rules/nl_kvk.py +265 -15
  12. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/METADATA +1 -1
  13. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/RECORD +26 -25
  14. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/WHEEL +1 -1
  15. tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +11 -15
  16. tests/resources/conformance_suites/dba/fr/fr7-invalid.xhtml +1 -1
  17. tests/resources/conformance_suites/dba/fr/fr83-invalid.xbrl +1 -1
  18. tests/resources/conformance_suites/dba/fr/fr89-testcase.xml +10 -0
  19. tests/resources/conformance_suites/dba/fr/fr89-valid.xhtml +6816 -0
  20. tests/resources/conformance_suites/dba/fr/fr91-invalid.xhtml +1 -2
  21. tests/resources/conformance_suites/dba/tm/tm29-invalid.xhtml +0 -1
  22. tests/resources/conformance_suites/dba/tm/tm31-invalid.xhtml +10 -1
  23. tests/resources/conformance_suites/dba/tr/tr06-invalid.xhtml +1 -1
  24. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/entry_points.txt +0 -0
  25. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/licenses/LICENSE.md +0 -0
  26. {arelle_release-2.37.13.dist-info → arelle_release-2.37.15.dist-info}/top_level.txt +0 -0
arelle/CntlrCmdLine.py CHANGED
@@ -622,7 +622,7 @@ class CntlrCmdLine(Cntlr.Cntlr):
622
622
  """
623
623
 
624
624
  def __init__(self, logFileName=None, uiLang=None, disable_persistent_config=False):
625
- super().__init__(hasGui=False, uiLang=uiLang, disable_persistent_config=disable_persistent_config)
625
+ super().__init__(hasGui=False, uiLang=uiLang, disable_persistent_config=disable_persistent_config, logFileName=logFileName)
626
626
  self.preloadedPlugins = {}
627
627
 
628
628
  def run(self, options: RuntimeOptions, sourceZipStream=None, responseZipStream=None, sourceZipStreamFileName=None) -> bool:
@@ -353,6 +353,13 @@ class ModelTestcaseVariation(ModelObject):
353
353
 
354
354
  return None
355
355
 
356
+ @property
357
+ def expectedWarnings(self):
358
+ warningElements = XmlUtil.descendants(self, None, "warning")
359
+ if isinstance(warningElements, list) and len(warningElements) > 0:
360
+ return [w.stringValue for w in warningElements]
361
+ return None
362
+
356
363
  @property
357
364
  def match(self) -> str | None:
358
365
  resultElement = XmlUtil.descendant(self, None, "result")
arelle/Validate.py CHANGED
@@ -710,6 +710,7 @@ class Validate:
710
710
  testcaseExpectedErrors = self.modelXbrl.modelManager.formulaOptions.testcaseExpectedErrors or {}
711
711
  matchAllExpected = testcaseResultOptions == "match-all" or modelTestcaseVariation.match == 'all'
712
712
  expectedReportCount = modelTestcaseVariation.expectedReportCount
713
+ expectedWarnings = modelTestcaseVariation.expectedWarnings if self.modelXbrl.modelManager.formulaOptions.testcaseResultsCaptureWarnings else []
713
714
  if expectedReportCount is not None and validateModelCount is not None and expectedReportCount != validateModelCount:
714
715
  errors.append("conf:testcaseExpectedReportCountError")
715
716
  _blockedMessageCodes = modelTestcaseVariation.blockedMessageCodes # restricts codes examined when provided
@@ -750,10 +751,21 @@ class Validate:
750
751
  status = "fail" if numErrors == 0 else "pass"
751
752
  elif expected in (None, []) and numErrors == 0:
752
753
  status = "pass"
753
- elif isinstance(expected,(QName,str,dict,list)): # string or assertion id counts dict
754
+ elif isinstance(expected, (QName, str, dict, list)) or expectedWarnings:
754
755
  status = "fail"
755
756
  _passCount = 0
756
- _expectedList = expected.copy() if isinstance(expected, list) else [expected]
757
+ if isinstance(expected, list):
758
+ _expectedList = expected.copy()
759
+ elif not expected:
760
+ _expectedList = []
761
+ else:
762
+ _expectedList = [expected]
763
+ if expectedWarnings:
764
+ _expectedList.extend(expectedWarnings)
765
+ if expectedCount is not None:
766
+ expectedCount += len(expectedWarnings)
767
+ else:
768
+ expectedCount = len(expectedWarnings)
757
769
  if not isinstance(expected, list):
758
770
  expected = [expected]
759
771
  for testErr in _errors:
@@ -380,7 +380,7 @@ def areFactsValueEqual(factA: ModelFact, factB: ModelFact) -> bool:
380
380
 
381
381
 
382
382
  def getAspectEqualFacts(
383
- hashEquivalentFacts: list[ModelFact], includeSingles: bool
383
+ hashEquivalentFacts: list[ModelFact], includeSingles: bool, useLang: bool = True
384
384
  ) -> Iterator[list[ModelFact]]:
385
385
  """
386
386
  Given a list of concept/context/unit hash-equivalent facts,
@@ -389,20 +389,14 @@ def getAspectEqualFacts(
389
389
  :param includeSingles: Whether to include lists of single facts (with no duplicates).
390
390
  :return: Lists of aspect-equal facts.
391
391
  """
392
- aspectEqualFacts: dict[
393
- tuple[QName, str | None], dict[tuple[ModelContext, ModelUnit], list[ModelFact]]
394
- ] = defaultdict(dict)
395
- for (
396
- fact
397
- ) in (
398
- hashEquivalentFacts
399
- ): # check for hash collision by value checks on context and unit
392
+ aspectEqualFacts: dict[tuple[QName, str | None], dict[tuple[ModelContext, ModelUnit], list[ModelFact]]] = defaultdict(dict)
393
+ for fact in hashEquivalentFacts: # check for hash collision by value checks on context and unit
400
394
  contextUnitDict = aspectEqualFacts[
401
395
  (
402
396
  fact.qname,
403
397
  (
404
398
  cast(str, fact.xmlLang or "").lower()
405
- if fact.concept.type.isWgnStringFactType
399
+ if useLang and fact.concept.type.isWgnStringFactType
406
400
  else None
407
401
  ),
408
402
  )
arelle/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.37.13'
21
- __version_tuple__ = version_tuple = (2, 37, 13)
20
+ __version__ = version = '2.37.15'
21
+ __version_tuple__ = version_tuple = (2, 37, 15)
@@ -13,6 +13,7 @@ from arelle.ModelInstanceObject import ModelFact, ModelContext
13
13
  from arelle.ModelValue import QName
14
14
  from arelle.ModelXbrl import ModelXbrl
15
15
  from arelle.utils.PluginData import PluginData
16
+ from arelle.XmlValidateConst import VALID
16
17
 
17
18
 
18
19
  @dataclass
@@ -47,7 +48,6 @@ class PluginValidationDataExtension(PluginData):
47
48
  consolidatedSoloDimensionQn: QName
48
49
  cpr_regex: regex.regex.Pattern[str]
49
50
  dateOfApprovalOfAnnualReportQn: QName
50
- dateOfApprovalOfReportQn: QName
51
51
  dateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod: QName
52
52
  dateOfGeneralMeetingQn: QName
53
53
  descriptionOfQualificationsOfAssuranceEngagementPerformedQn: QName
@@ -157,3 +157,11 @@ class PluginValidationDataExtension(PluginData):
157
157
  contexts.append(context)
158
158
  self._reportingPeriodContexts = sorted(contexts, key=lambda c: c.endDatetime)
159
159
  return self._reportingPeriodContexts
160
+
161
+ def isAnnualReport(self, modelXbrl: ModelXbrl) -> bool:
162
+ """
163
+ :return: Return True if Type of Submitted Report value is in the annual report types
164
+ """
165
+ reportTypeFacts = modelXbrl.factsByQname.get(self.informationOnTypeOfSubmittedReportQn, set())
166
+ filteredReportTypeFacts = [f for f in reportTypeFacts if f.xValid >= VALID and f.xValue in self.annualReportTypes]
167
+ return len(filteredReportTypeFacts) > 0
@@ -161,7 +161,10 @@ class ValidationPluginExtension(ValidationPlugin):
161
161
  annualReportTypes=frozenset([
162
162
  'Årsrapport',
163
163
  'årsrapport',
164
- 'Annual report'
164
+ 'Annual report',
165
+ 'Likvidationsregnskab',
166
+ 'likvidationsregnskab',
167
+ 'Liquidation accounts',
165
168
  ]),
166
169
  assetsQn=qname(f'{{{NAMESPACE_FSA}}}Assets'),
167
170
  auditedAssuranceReportsDanish='Andre erklæringer med sikkerhed',
@@ -282,7 +285,6 @@ class ValidationPluginExtension(ValidationPlugin):
282
285
  consolidatedSoloDimensionQn=qname(f'{{{NAMESPACE_CMN}}}ConsolidatedSoloDimension'),
283
286
  cpr_regex=re.compile(r'^([0-2][0-9]|3[0-1])(0[1-9]|1[0-2])[0-9]{2}-[0-9]{4}'),
284
287
  dateOfApprovalOfAnnualReportQn=qname(f'{{{NAMESPACE_SOB}}}DateOfApprovalOfAnnualReport'),
285
- dateOfApprovalOfReportQn=qname(f'{{{NAMESPACE_GSD}}}DateOfApprovalOfReport'),
286
288
  dateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod=qname(f'{{{NAMESPACE_FSA}}}DateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod'),
287
289
  dateOfGeneralMeetingQn=qname(f'{{{NAMESPACE_GSD}}}DateOfGeneralMeeting'),
288
290
  declarationObligationQns=frozenset([
@@ -1266,29 +1266,26 @@ def rule_fr89(
1266
1266
  Regnskabsklasse D // Reporting class D
1267
1267
  Then TypeOfAuditorAssistance should be: Revisionspåtegning // Auditor's report on audited financial statements
1268
1268
  """
1269
- modelXbrl = val.modelXbrl
1270
- auditorFacts = modelXbrl.factsByQname.get(pluginData.typeOfAuditorAssistanceQn)
1271
- if auditorFacts is not None:
1269
+ if pluginData.isAnnualReport(val.modelXbrl):
1270
+ auditorFacts = val.modelXbrl.factsByQname.get(pluginData.typeOfAuditorAssistanceQn, set())
1272
1271
  for auditorFact in auditorFacts:
1273
1272
  if auditorFact.xValid >= VALID and auditorFact.xValue in [
1274
1273
  pluginData.auditedFinancialStatementsDanish,
1275
1274
  pluginData.auditedFinancialStatementsEnglish
1276
1275
  ]:
1277
1276
  return
1278
- classFacts = []
1279
- facts = modelXbrl.factsByQname.get(pluginData.classOfReportingEntityQn)
1280
- if facts is not None:
1277
+ classFacts = []
1278
+ facts = val.modelXbrl.factsByQname.get(pluginData.classOfReportingEntityQn, set())
1281
1279
  for fact in facts:
1282
- if fact.xValid >= VALID:
1283
- if fact.xValue in [
1284
- pluginData.reportingClassCLargeDanish,
1285
- pluginData.reportingClassCLargeEnglish,
1286
- pluginData.reportingClassCMediumDanish,
1287
- pluginData.reportingClassCMediumEnglish,
1288
- pluginData.reportingClassDDanish,
1289
- pluginData.reportingClassDEnglish,
1290
- ]:
1291
- classFacts.append(fact)
1280
+ if fact.xValid >= VALID and fact.xValue in [
1281
+ pluginData.reportingClassCLargeDanish,
1282
+ pluginData.reportingClassCLargeEnglish,
1283
+ pluginData.reportingClassCMediumDanish,
1284
+ pluginData.reportingClassCMediumEnglish,
1285
+ pluginData.reportingClassDDanish,
1286
+ pluginData.reportingClassDEnglish
1287
+ ]:
1288
+ classFacts.append(fact)
1292
1289
  if len(classFacts) > 0:
1293
1290
  yield Validation.error(
1294
1291
  codes='DBA.FR89',
@@ -1313,22 +1310,23 @@ def rule_fr91(
1313
1310
  ) -> Iterable[Validation]:
1314
1311
  """
1315
1312
  DBA.FR91: If the annual report contains information about both the general meeting date
1316
- (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfReport), the values must be the same.
1317
- """
1318
- approvalOfReportFact = None
1319
- generalMeetingFact = None
1320
- approvalFacts = (val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfReportQn, set()))
1321
- if len(approvalFacts) > 0:
1322
- approvalOfReportFact = next(iter(approvalFacts), None)
1323
- meetingFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
1324
- if len(meetingFacts) > 0:
1325
- generalMeetingFact = next(iter(meetingFacts), None)
1326
- if generalMeetingFact is not None and generalMeetingFact.xValid >= VALID and approvalOfReportFact is not None and approvalOfReportFact.xValid >= VALID and generalMeetingFact.xValue != approvalOfReportFact.xValue:
1327
- yield Validation.error(
1328
- codes='DBA.FR91',
1329
- msg=_("The annual report contains information about both the general meeting date (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfReport), the values must be the same."),
1330
- modelObject=[generalMeetingFact, approvalOfReportFact]
1331
- )
1313
+ (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfAnnualReport), the values must be the same.
1314
+ """
1315
+ if pluginData.isAnnualReport(val.modelXbrl):
1316
+ approvalOfReportFact = None
1317
+ generalMeetingFact = None
1318
+ approvalFacts = (val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set()))
1319
+ if len(approvalFacts) > 0:
1320
+ approvalOfReportFact = next(iter(approvalFacts), None)
1321
+ meetingFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
1322
+ if len(meetingFacts) > 0:
1323
+ generalMeetingFact = next(iter(meetingFacts), None)
1324
+ if generalMeetingFact is not None and generalMeetingFact.xValid >= VALID and approvalOfReportFact is not None and approvalOfReportFact.xValid >= VALID and generalMeetingFact.xValue != approvalOfReportFact.xValue:
1325
+ yield Validation.error(
1326
+ codes='DBA.FR91',
1327
+ msg=_("The annual report contains information about both the general meeting date (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfAnnualReport), the values must be the same."),
1328
+ modelObject=[generalMeetingFact, approvalOfReportFact]
1329
+ )
1332
1330
 
1333
1331
 
1334
1332
  @validation(
@@ -239,16 +239,16 @@ def rule_tm29(
239
239
  **kwargs: Any,
240
240
  ) -> Iterable[Validation]:
241
241
  """
242
- DBA.TM29: Either gsd:DateOfGeneralMeeting or gsd:DateOfApprovalOfReport must be specified
242
+ DBA.TM29: Either gsd:DateOfGeneralMeeting or gsd:DateOfApprovalOfAnnualReport must be specified
243
243
  """
244
- modelXbrl = val.modelXbrl
245
- meeting_facts = modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
246
- approval_facts = modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfReportQn, set())
247
- if len(meeting_facts) == 0 and len(approval_facts) == 0:
248
- yield Validation.error(
249
- 'DBA.TM29',
250
- _('Either DateOfGeneralMeeting or DateOfApprovalOfReport must be tagged in the document.')
251
- )
244
+ if pluginData.isAnnualReport(val.modelXbrl):
245
+ meeting_facts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
246
+ approval_facts = val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set())
247
+ if len(meeting_facts) == 0 and len(approval_facts) == 0:
248
+ yield Validation.error(
249
+ 'DBA.TM29',
250
+ _('Either DateOfGeneralMeeting or DateOfApprovalOfReport must be tagged in the document.')
251
+ )
252
252
 
253
253
 
254
254
  @validation(
@@ -276,9 +276,19 @@ def rule_tm31(
276
276
  **kwargs: Any,
277
277
  ) -> Iterable[Validation]:
278
278
  """
279
- DBA.TM31: gsd:DateOfApprovalOfReport must only be tagged once if tagged
280
- """
281
- return errorOnMultipleFacts(val.modelXbrl, pluginData.dateOfApprovalOfReportQn, 'DBA.TM31')
279
+ DBA.TM31: gsd:DateOfApprovalOfAnnualReport must only be tagged once if tagged
280
+ """
281
+ if pluginData.isAnnualReport(val.modelXbrl):
282
+ dateFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set())
283
+ if len(dateFacts) > 1:
284
+ yield Validation.error(
285
+ 'DBA.TM31',
286
+ _('{} must only be tagged once. {} facts were found.').format(
287
+ pluginData.dateOfApprovalOfAnnualReportQn.localName,
288
+ len(dateFacts)
289
+ ),
290
+ modelObject=dateFacts
291
+ )
282
292
 
283
293
 
284
294
  @validation(
@@ -6,20 +6,23 @@ from __future__ import annotations
6
6
  from collections import defaultdict
7
7
  from dataclasses import dataclass
8
8
  from functools import lru_cache
9
+ from pathlib import Path
9
10
  from typing import Any, TYPE_CHECKING, cast
10
11
 
11
12
  import regex as re
12
13
  from lxml.etree import _Comment, _ElementTree, _Entity, _ProcessingInstruction
13
14
 
14
15
  from arelle.FunctionIxt import ixtNamespaces
15
- from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit
16
+ from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit, ModelInlineFact
16
17
  from arelle.ModelObject import ModelObject
17
18
  from arelle.ModelValue import QName
18
19
  from arelle.ModelXbrl import ModelXbrl
19
20
  from arelle.typing import assert_type
20
21
  from arelle.utils.PluginData import PluginData
21
22
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
23
+ from arelle.XbrlConst import ixbrl11
22
24
  from arelle.XmlValidate import lexicalPatterns
25
+ from arelle.XmlValidateConst import VALID
23
26
 
24
27
  XBRLI_IDENTIFIER_PATTERN = re.compile(r"^(?!00)\d{8}$")
25
28
  XBRLI_IDENTIFIER_SCHEMA = 'http://www.kvk.nl/kvk-id'
@@ -29,6 +32,25 @@ DISALLOWED_IXT_NAMESPACES = frozenset((
29
32
  ixtNamespaces["ixt v2"],
30
33
  ixtNamespaces["ixt v3"],
31
34
  ))
35
+ UNTRANSFORMABLE_TYPES = frozenset((
36
+ "anyURI",
37
+ "base64Binary",
38
+ "duration",
39
+ "hexBinary",
40
+ "NOTATION",
41
+ "QName",
42
+ "time",
43
+ "token",
44
+ "language",
45
+ ))
46
+ STYLE_IX_HIDDEN_PATTERN = re.compile(r"(.*[^\w]|^)ix-hidden\s*:\s*([\w.-]+).*")
47
+
48
+ ALLOWABLE_LANGUAGES = frozenset((
49
+ 'nl',
50
+ 'en',
51
+ 'de',
52
+ 'fr'
53
+ ))
32
54
 
33
55
  @dataclass(frozen=True)
34
56
  class ContextData:
@@ -38,10 +60,18 @@ class ContextData:
38
60
  contextsWithSegments: list[ModelContext | None]
39
61
 
40
62
  @dataclass(frozen=True)
41
- class FootnoteData:
42
- factLangFootnotes: dict[ModelObject, set[str]]
63
+ class HiddenElementsData:
64
+ eligibleForTransformHiddenFacts: set[ModelInlineFact]
65
+ hiddenFactsOutsideHiddenSection: set[ModelInlineFact]
66
+ requiredToDisplayFacts: set[ModelInlineFact]
67
+
68
+ @dataclass(frozen=True)
69
+ class InlineHTMLData:
43
70
  noMatchLangFootnotes: set[ModelInlineFootnote]
44
71
  orphanedFootnotes: set[ModelInlineFootnote]
72
+ tupleElements: set[tuple[Any]]
73
+ factLangFootnotes: dict[ModelInlineFootnote, set[str]]
74
+ fractionElements: set[Any]
45
75
 
46
76
  @dataclass
47
77
  class PluginValidationDataExtension(PluginData):
@@ -103,33 +133,83 @@ class PluginValidationDataExtension(PluginData):
103
133
  )
104
134
 
105
135
  @lru_cache(1)
106
- def checkFootnotes(self, modelXbrl: ModelXbrl) -> FootnoteData:
136
+ def checkHiddenElements(self, modelXbrl: ModelXbrl) -> HiddenElementsData:
137
+ eligibleForTransformHiddenFacts = set()
138
+ hiddenEltIds = {}
139
+ hiddenFactsOutsideHiddenSection = set()
140
+ presentedHiddenEltIds = defaultdict(list)
141
+ requiredToDisplayFacts = set()
142
+ for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
143
+ ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
144
+ for ixHiddenElt in ixdsHtmlRootElt.iterdescendants(tag=ixNStag + "hidden"):
145
+ for tag in (ixNStag + "nonNumeric", ixNStag+"nonFraction"):
146
+ for ixElt in ixHiddenElt.iterdescendants(tag=tag):
147
+ if getattr(ixElt, "xValid", 0) >= VALID:
148
+ if ixElt.concept.baseXsdType not in UNTRANSFORMABLE_TYPES and not ixElt.isNil:
149
+ eligibleForTransformHiddenFacts.add(ixElt)
150
+ if ixElt.id:
151
+ hiddenEltIds[ixElt.id] = ixElt
152
+ for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
153
+ for ixElt in ixdsHtmlRootElt.getroottree().iterfind(".//{http://www.w3.org/1999/xhtml}*[@style]"):
154
+ styleValue = ixElt.get("style","")
155
+ hiddenFactRefMatch = STYLE_IX_HIDDEN_PATTERN.match(styleValue)
156
+ if hiddenFactRefMatch:
157
+ hiddenFactRef = hiddenFactRefMatch.group(2)
158
+ if hiddenFactRef not in hiddenEltIds:
159
+ hiddenFactsOutsideHiddenSection.add(ixElt)
160
+ else:
161
+ presentedHiddenEltIds[hiddenFactRef].append(ixElt)
162
+ for hiddenEltId, ixElt in hiddenEltIds.items():
163
+ if (hiddenEltId not in presentedHiddenEltIds and
164
+ getattr(ixElt, "xValid", 0) >= VALID and # may not be validated
165
+ (ixElt.concept.baseXsdType in UNTRANSFORMABLE_TYPES or ixElt.isNil)):
166
+ requiredToDisplayFacts.add(ixElt)
167
+ return HiddenElementsData(
168
+ eligibleForTransformHiddenFacts=eligibleForTransformHiddenFacts,
169
+ hiddenFactsOutsideHiddenSection=hiddenFactsOutsideHiddenSection,
170
+ requiredToDisplayFacts=requiredToDisplayFacts,
171
+ )
172
+
173
+ @lru_cache(1)
174
+ def checkInlineHTMLElements(self, modelXbrl: ModelXbrl) -> InlineHTMLData:
107
175
  factLangs = self.factLangs(modelXbrl)
108
176
  footnotesRelationshipSet = modelXbrl.relationshipSet("XBRL-footnotes")
109
177
  factLangFootnotes = defaultdict(set)
110
- orphanedFootnotes = set()
178
+ fractionElements = set()
111
179
  noMatchLangFootnotes = set()
112
- for elts in modelXbrl.ixdsEltById.values(): # type: ignore[attr-defined]
113
- for elt in elts:
114
- if isinstance(elt, ModelInlineFootnote):
115
- if elt.textValue is not None:
116
- if not any(isinstance(rel.fromModelObject, ModelFact)
117
- for rel in footnotesRelationshipSet.toModelObject(elt)):
118
- orphanedFootnotes.add(elt)
119
- if elt.xmlLang not in factLangs:
120
- noMatchLangFootnotes.add(elt)
121
- if elt.xmlLang is not None:
122
- for rel in footnotesRelationshipSet.toModelObject(elt):
123
- if rel.fromModelObject is not None:
124
- fromObj = cast(ModelObject, rel.fromModelObject)
125
- lang = cast(str, elt.xmlLang)
126
- factLangFootnotes[fromObj].add(lang)
180
+ tupleElements = set()
181
+ orphanedFootnotes = set()
182
+ for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
183
+ ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
184
+ ixTupleTag = ixNStag + "tuple"
185
+ ixFractionTag = ixNStag + "fraction"
186
+ for elts in modelXbrl.ixdsEltById.values(): # type: ignore[attr-defined]
187
+ for elt in elts:
188
+ if isinstance(elt, ModelInlineFootnote):
189
+ if elt.textValue is not None:
190
+ if not any(isinstance(rel.fromModelObject, ModelFact)
191
+ for rel in footnotesRelationshipSet.toModelObject(elt)):
192
+ orphanedFootnotes.add(elt)
193
+ if elt.xmlLang not in factLangs:
194
+ noMatchLangFootnotes.add(elt)
195
+ if elt.xmlLang is not None:
196
+ for rel in footnotesRelationshipSet.toModelObject(elt):
197
+ if rel.fromModelObject is not None:
198
+ fromObj = cast(ModelObject, rel.fromModelObject)
199
+ lang = cast(str, elt.xmlLang)
200
+ factLangFootnotes[fromObj].add(lang)
201
+ if elt.tag == ixTupleTag:
202
+ tupleElements.add(elt)
203
+ if elt.tag == ixFractionTag:
204
+ fractionElements.add(elt)
127
205
  factLangFootnotes.default_factory = None
128
206
  assert_type(factLangFootnotes, defaultdict[ModelObject, set[str]])
129
- return FootnoteData(
130
- factLangFootnotes=cast(dict[ModelObject, set[str]], factLangFootnotes),
207
+ return InlineHTMLData(
208
+ factLangFootnotes=cast(dict[ModelInlineFootnote, set[str]], factLangFootnotes),
209
+ fractionElements=fractionElements,
131
210
  noMatchLangFootnotes=noMatchLangFootnotes,
132
211
  orphanedFootnotes=orphanedFootnotes,
212
+ tupleElements=tupleElements,
133
213
  )
134
214
 
135
215
  @lru_cache(1)
@@ -164,14 +244,57 @@ class PluginValidationDataExtension(PluginData):
164
244
  def getContextsWithSegments(self, modelXbrl: ModelXbrl) -> list[ModelContext | None]:
165
245
  return self.checkContexts(modelXbrl).contextsWithSegments
166
246
 
167
- def getFactLangFootnotes(self, modelXbrl: ModelXbrl) -> dict[ModelObject, set[str]]:
168
- return self.checkFootnotes(modelXbrl).factLangFootnotes
247
+ def getEligibleForTransformHiddenFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
248
+ return self.checkHiddenElements(modelXbrl).eligibleForTransformHiddenFacts
249
+
250
+ def getFactLangFootnotes(self, modelXbrl: ModelXbrl) -> dict[ModelInlineFootnote, set[str]]:
251
+ return self.checkInlineHTMLElements(modelXbrl).factLangFootnotes
252
+
253
+ def getFractionElements(self, modelXbrl: ModelXbrl) -> set[Any]:
254
+ return self.checkInlineHTMLElements(modelXbrl).fractionElements
255
+
256
+ def getHiddenFactsOutsideHiddenSection(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
257
+ return self.checkHiddenElements(modelXbrl).hiddenFactsOutsideHiddenSection
258
+
259
+ @lru_cache(1)
260
+ def getFilenameAllowedCharactersPattern(self) -> re.Pattern[str]:
261
+ return re.compile(
262
+ r"^[\w\.-]*$",
263
+ flags=re.ASCII
264
+ )
265
+
266
+ @lru_cache(1)
267
+ def getFilenameFormatPattern(self) -> re.Pattern[str]:
268
+ return re.compile(
269
+ r"^(?<base>[^-]*)"
270
+ r"-(?<year>\d{4})-(?<month>0[1-9]|1[012])-(?<day>0?[1-9]|[12][0-9]|3[01])"
271
+ r"-(?<lang>[^-]*)"
272
+ r"\.(?<extension>html|htm|xhtml)$",
273
+ flags=re.ASCII
274
+ )
275
+
276
+ @lru_cache(1)
277
+ def getFilenameParts(self, filename: str) -> dict[str, Any] | None:
278
+ match = self.getFilenameFormatPattern().match(filename)
279
+ if match:
280
+ return match.groupdict()
281
+ return None
282
+
283
+ @lru_cache(1)
284
+ def getIxdsDocBasenames(self, modelXbrl: ModelXbrl) -> set[str]:
285
+ return set(Path(url).name for url in modelXbrl.ixdsDocUrls)
169
286
 
170
287
  def getNoMatchLangFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
171
- return self.checkFootnotes(modelXbrl).noMatchLangFootnotes
288
+ return self.checkInlineHTMLElements(modelXbrl).noMatchLangFootnotes
172
289
 
173
290
  def getOrphanedFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
174
- return self.checkFootnotes(modelXbrl).orphanedFootnotes
291
+ return self.checkInlineHTMLElements(modelXbrl).orphanedFootnotes
292
+
293
+ def getRequiredToDisplayFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
294
+ return self.checkHiddenElements(modelXbrl).requiredToDisplayFacts
295
+
296
+ def getTupleElements(self, modelXbrl: ModelXbrl) -> set[tuple[Any]]:
297
+ return self.checkInlineHTMLElements(modelXbrl).tupleElements
175
298
 
176
299
  @lru_cache(1)
177
300
  def getReportXmlLang(self, modelXbrl: ModelXbrl) -> str | None:
@@ -188,6 +311,11 @@ class PluginValidationDataExtension(PluginData):
188
311
  firstRootmostXmlLangDepth = depth
189
312
  return reportXmlLang
190
313
 
314
+ @lru_cache(1)
315
+ def isFilenameValidCharacters(self, filename: str) -> bool:
316
+ match = self.getFilenameAllowedCharactersPattern().match(filename)
317
+ return match is not None
318
+
191
319
  @lru_cache(1)
192
320
  def unitsByDocument(self, modelXbrl: ModelXbrl) -> dict[str, list[ModelUnit]]:
193
321
  unitsByDocument = defaultdict(list)