Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/.settings
/.pydevproject
/.vscode
/view/rfileviewer/node_modules
/view/rfileviewer/package-lock.json

4 changes: 4 additions & 0 deletions __builtins__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# __builtins__.pyi
from typing import Callable

_ = Callable[[str], str]
6 changes: 3 additions & 3 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
__pluginInfo__ = {
'name': 'EDGAR',
'version': '3.26.1',
'description': "This plug-in implements U.S. SEC Edgar Renderer. Arelle version at SEC: 2.39.8 ",
'version': '3.26.3',
'description': "This plug-in implements U.S. SEC Edgar Renderer. Arelle version at SEC: 2.44.2 ",
'license': 'Apache-2',
'author': 'U.S. SEC Employees and Mark V Systems Limited',
'copyright': '(c) Portions by SEC Employees not subject to domestic copyright, otherwise (c) Copyright 2015 Mark V Systems Limited, All rights reserved.',
'import': ('EDGAR/render', )
'import': ('EDGAR/render', 'EDGAR/validate')
}
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
The EDGAR plugin, developed and maintained by the staff of the U.S. Securities and Exchange Commission (SEC), is designed to provide traditional and inline XBRL viewers for SEC filings. It also integrates with and extends the EFM Validation plugin, offering EFM validation for SEC filings. For end-user support, please contact the SEC directly at: StructuredData@sec.gov.

## Arelle Version
The current version of Arelle in use at the SEC is: **2.39.8**
The current version of Arelle in use at the SEC is: **2.44.2**

## Installation
The EDGAR plugin requires the xule plugin to be present under the Arelle plugin directory. You can clone the xule repository into the plugin directory.
Expand Down
4 changes: 2 additions & 2 deletions render/Embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,15 @@ def processOrFilterFacts(self):
for typedMember in sorted((getMemberOnAxisForFactDict[pseudoAxis]
for fact, getMemberOnAxisForFactDict, periodStartEndLabel in self.cube.factMemberships
if pseudoAxis in getMemberOnAxisForFactDict),
key=lambda member: member.typedMemberSortKey):
key=lambda member: member.typedMemberSortKey if isinstance(member,Filing.Member) else str(member)):
if typedMember not in giveMemGetPositionDict:
giveMemGetPositionDict[typedMember] = len(giveMemGetPositionDict)

except TypeError: # if unsortable members, try as string (but will be inconsistent on numbers)
for typedMember in sorted((getMemberOnAxisForFactDict[pseudoAxis]
for fact, getMemberOnAxisForFactDict, periodStartEndLabel in self.cube.factMemberships
if pseudoAxis in getMemberOnAxisForFactDict),
key=lambda member: str(member.typedMemberSortKey)):
key=lambda member: str(member.typedMemberSortKey) if isinstance(member,Filing.Member) else str(member)):
if typedMember not in giveMemGetPositionDict:
giveMemGetPositionDict[typedMember] = len(giveMemGetPositionDict)

Expand Down
6 changes: 6 additions & 0 deletions render/Filing.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,12 @@ def populateAndLinkClasses(self, uncategorizedCube=None):
facts = list(self.unusedFactSet)

else:
# clear fact.inCubes from any prior Filing.mainFun call on this modelXbrl.
# Not clearing them could cause RemoveStuntedCashFlowColumns to treat facts as appearing in other reports.
for fact in self.modelXbrl.facts:
if hasattr(fact, 'inCubes'):
fact.inCubes.clear()

# build cubes
for linkroleUri in self.modelXbrl.relationshipSet(arelle.XbrlConst.parentChild).linkRoleUris:
cube = Cube.Cube(self, linkroleUri)
Expand Down
16 changes: 8 additions & 8 deletions render/IoManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ def absPathOnPythonPath(controller, filename): # if filename is relative, find
controller.logDebug("No such location {} found in sys path dirs {}.".format(filename, pathdirs))
return None

def writeTextDoc(filing, text, reportZip, reportFolder, filename, zipDir="", encoding=None):
if reportZip:
reportZip.writestr(zipDir + filename, text)
elif reportFolder is not None:
filing.writeFile(os.path.join(reportFolder, filename), text, encoding=encoding)


def writeXmlDoc(filing, etree, reportZip, reportFolder, filename, zipDir=""):
xmlText = treeToString(etree.getroottree(), method='xml', with_tail=False, pretty_print=True, encoding='utf-8', xml_declaration=True)
if reportZip:
reportZip.writestr(zipDir + filename, xmlText)
elif reportFolder is not None:
filing.writeFile(os.path.join(reportFolder, filename), xmlText)
writeTextDoc(filing, xmlText, reportZip, reportFolder, filename, zipDir=zipDir)


def writeHtmlDoc(filing, root, reportZip, reportFolder, filename, zipDir=""):
htmlText = treeToString(root, method='html', with_tail=False, pretty_print=True, encoding='utf-8')
if reportZip:
reportZip.writestr(zipDir + filename, htmlText)
elif reportFolder is not None:
filing.writeFile(os.path.join(reportFolder, filename), htmlText)
writeTextDoc(filing, htmlText, reportZip, reportFolder, filename, zipDir=zipDir)


def writeJsonDoc(lines, pathOrStream, sort_keys=True):
Expand Down
8 changes: 4 additions & 4 deletions render/PresentationGroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,13 @@ def doPreorderTraversal(self, node, giveMemGetPositionDictPrimary, giveMemGetPos
if depth <= len(visited): # you can't go deeper than the number of unique relationships you've already visited.
for childNode in node.childrenList:
if parentAxis is not None: # collect more of this axis' descendants
self.doPreorderTraversal(childNode, giveMemGetPositionDictPrimary, giveMemGetPositionDictAxis, parentAxis, setOfConcepts, visited, visitCounter, depth)
self.doPreorderTraversal(childNode, giveMemGetPositionDictPrimary, giveMemGetPositionDictAxis, parentAxis, setOfConcepts, visited, visitCounter, depth + 1)
else:
self.doPreorderTraversal(childNode, giveMemGetPositionDictPrimary, {}, parentAxis, setOfConcepts, visited, visitCounter, depth)
self.doPreorderTraversal(childNode, giveMemGetPositionDictPrimary, {}, parentAxis, setOfConcepts, visited, visitCounter, depth + 1)
else:
self.filing.modelXbrl.debug("info",
("Presentation group '%{linkRoleName} a an invalid directed cycle"),
linkrole=self.cube.linkroleUri)
("Presentation group \"%(linkRoleName)s\" has an invalid directed cycle"),
linkRoleName=self.cube.linkroleUri)

if nodeIsAnAxis:
if concept.isTypedDimension: # designate this as a typed dimension axis
Expand Down
2 changes: 1 addition & 1 deletion render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ domestic copyright protection. 17 U.S.C. 105.
End user support is by e-mail direct to SEC at: [StructuredData@sec.gov]
(mailto:StructuredData@sec.gov).

This is EDGAR release 26.1, planned for production March, 2026.
This is EDGAR release 26.3, planned for production September, 2026.

Developer issue management is by the Jira Edgar Renderer project: https://arelle.atlassian.net/projects/ER

Expand Down
183 changes: 183 additions & 0 deletions render/RFileViewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
"""
:mod:`EdgarRenderer.Summary`
Edgar(tm) Renderer was created by staff of the U.S. Securities and Exchange Commission.
Data and content created by government employees within the scope of their employment
are not subject to domestic copyright protection. 17 U.S.C. 105.
"""

# provide choice of python Javascript libraries
JS_LIB = "pythonmonkey" # 27 Mb extra distribution size, robust implementation for node.js
#JS_LIB = "quickjs" # 3 Mb extra distribution size, customary quickjs (dormant project, no recent activity)
#JS_LIB = "quickjs-ng" # active quickjs project under maintenance, need PR 8 (https://github.com/genotrance/quickjs-ng/pull/8) to handle long strings for FilingSummary.xml
if JS_LIB == "pythonmonkey":
from pythonmonkey import eval as js_eval
elif JS_LIB == "quickjs":
import quickjs
elif JS_LIB == "quickjs-ng": # unsure, has to be built from PR to quickns-ng
import quickjs

from lxml import etree
import json, os

JS_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "view", "rfileviewer", "dist", "r-file-viewer.transform.iife.js")


# this section implements an lxml.etree to fast-xml-parser object for python

ATTR_PREFIX = ""
ATTRS_GRP_NAME = "_attributes"
TEXT_KEY = "_text"

def _coerce_scalar(s: str):
"""Coerce strings to bool/int/float when appropriate, else return as str."""
if s is None:
return ""
s = s.strip()
# boolean
if s.lower() == "true":
return True
if s.lower() == "false":
return False
# integer
if s.isdigit() or (s.startswith("-") and s[1:].isdigit()):
try:
return int(s)
except ValueError:
pass
# float
try:
# allow numbers like 1.23 or -0.5
if any(ch in s for ch in ".eE"):
return float(s)
except ValueError:
pass
return s

def _merge_child(d: dict, tag: str, value):
"""Place child value under tag, making a list only if tag repeats."""
if tag in d:
# Promote to list if repeated
if isinstance(d[tag], list):
d[tag].append(value)
else:
d[tag] = [d[tag], value]
else:
d[tag] = value

def lxml_element_to_fxp_json(el: etree._Element):
"""
Convert any lxml element to a dict shaped like fast-xml-parser JSON output:
- attributes in child object ATTRS_GRP_NAME if not null, prefixed with ATTR_PREFIX
- text under TEXT_KEY only when attributes or children exist
- scalars for simple elements (no attrs/children)
- lists created only for repeated sibling tag names
"""
# Collect attributes (prefixed)
out = {}
for k, v in el.attrib.items():
if ATTRS_GRP_NAME:
if ATTRS_GRP_NAME not in out: out[ATTRS_GRP_NAME] = {}
out[ATTRS_GRP_NAME][f"{ATTR_PREFIX}{k}"] = _coerce_scalar(v)
else:
out[f"{ATTR_PREFIX}{k}"] = _coerce_scalar(v)

# Recurse over children, grouping by tag name
child_tag_counts = {}
for child in el:
child_tag_counts[child.tag] = child_tag_counts.get(child.tag, 0) + 1

for child in el:
child_val = lxml_element_to_fxp_json(child)
# If child is simple (returns scalar), keep as scalar
# Otherwise it's an object (possibly containing attributes/TEXT_KEY)
_merge_child(out, child.tag, child_val)

# Handle element text
text = (el.text or "").strip()
has_children = len(el) > 0
has_attrs = bool(el.attrib)

if has_children or has_attrs:
# If there's meaningful text alongside attrs/children, store under TEXT_KEY
if text:
out[TEXT_KEY] = _coerce_scalar(text)
# If no text and no children produced anything, ensure at least {} is returned
if not out:
out = {}
return out
else:
# Simple element → scalar
return _coerce_scalar(text)

def lxml_etree_to_fxp_json(root: etree._Element):
"""Top-level wrapper returning {root.tag: ...}"""
return {root.tag: lxml_element_to_fxp_json(root)}

# entry point from render/__init__.py to transform etree into html

def transformToHtml(filing_summary_etree, accession_number, title=None, timeout_sec=60, logDebugToConsole=False, secws=False):

# transform FilingSummary lxml etree to fast-xml-parser JSON object
fxp_json_obj = lxml_etree_to_fxp_json(filing_summary_etree)

# Load the transform-only bundle
with open(JS_PATH, "r", encoding="utf-8") as f:
js_code = f.read()

if JS_LIB == "pythonmonkey":
js_eval(js_code)
js_transform = js_eval("RFileViewer.transform")

try:
html = js_transform(
fxp_json_obj,
accession_number,
title,
logDebugToConsole,
secws
)
return html
except Exception as exc:
print("PythonMonkey JS error:", exc)
raise

elif JS_LIB in ("quickjs", "quickjs-ng"):
# quickjs needs stringified object parameter
fxp_json_str = json.dumps(fxp_json_obj)

# add sourceURL for better stack traces
js_code = "//@ sourceURL=r-file-viewer.transform.iife.js\n" + js_code

ctx = quickjs.Context()
try:
# Evaluate IIFE bundle
ctx.eval(js_code)
# Create a wrapper for easier calling
# Wrap the transform with JSON.parse inside JS
ctx.eval("""
function __rfv_transform(fxp_json_str, accession_number, title, logDebugToConsole, secws) {
const fxp_json_obj = JSON.parse(fxp_json_str);
return RFileViewer.transform(fxp_json_obj, accession_number, title, logDebugToConsole, secws);
}
""")

# Retrieve JS function reference (Python callable)
ctx.set_time_limit(int(timeout_sec * 1000))
transform_fn = ctx.get("__rfv_transform")

# Call JS function
html = transform_fn(
fxp_json_str,
accession_number,
title,
logDebugToConsole,
secws
)
return html
except quickjs.JSException as exc:
# Log JS exception with stack trace
print("JS exception in RFV transform:", exc)
print("JS traceback:\n", str(exc))
# Optionally re-raise or wrap as needed
raise
Loading