Coverage for src/pyTRLCConverter/reqif_converter.py: 95%

478 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-26 12:41 +0000

1"""Converter to ReqIF format. 

2 

3 Author: Andreas Merkle (andreas.merkle@newtec.de) 

4""" 

5 

6# pyTRLCConverter - A tool to convert TRLC files to specific formats. 

7# Copyright (c) 2024 - 2026 NewTec GmbH 

8# 

9# This file is part of pyTRLCConverter program. 

10# 

11# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software Foundation, 

13# either version 3 of the License, or (at your option) any later version. 

14# 

15# The pyTRLCConverter program is distributed in the hope that it will be useful, but 

16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

17# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License along with pyTRLCConverter. 

20# If not, see <https://www.gnu.org/licenses/>. 

21 

22# Imports ********************************************************************** 

23import html 

24import mimetypes 

25import os 

26import re 

27import shutil 

28import tempfile 

29import zipfile 

30from datetime import datetime, timezone 

31from typing import Any, Optional 

32from marko import Markdown 

33from reqif.object_lookup import ReqIFObjectLookup 

34from reqif.reqif_bundle import ReqIFBundle 

35from reqif.unparser import ReqIFUnparser 

36from reqif.models.reqif_core_content import ReqIFCoreContent 

37from reqif.models.reqif_data_type import ( 

38 ReqIFDataTypeDefinitionEnumeration, 

39 ReqIFDataTypeDefinitionString, 

40 ReqIFDataTypeDefinitionXHTML, 

41 ReqIFEnumValue 

42) 

43from reqif.models.reqif_namespace_info import ReqIFNamespaceInfo 

44from reqif.models.reqif_reqif_header import ReqIFReqIFHeader 

45from reqif.models.reqif_req_if_content import ReqIFReqIFContent 

46from reqif.models.reqif_spec_hierarchy import ReqIFSpecHierarchy 

47from reqif.models.reqif_spec_object import ReqIFSpecObject, SpecObjectAttribute 

48from reqif.models.reqif_spec_object_type import ReqIFSpecObjectType, SpecAttributeDefinition 

49from reqif.models.reqif_spec_relation import ReqIFSpecRelation 

50from reqif.models.reqif_spec_relation_type import ReqIFSpecRelationType 

51from reqif.models.reqif_specification import ReqIFSpecification 

52from reqif.models.reqif_specification_type import ReqIFSpecificationType 

53from reqif.models.reqif_types import SpecObjectAttributeType 

54from trlc.ast import ( 

55 Array_Aggregate, Enumeration_Literal, Enumeration_Type, Implicit_Null, 

56 Record_Object, Record_Reference, String_Literal, Expression, Symbol_Table 

57) 

58from pyTRLCConverter.base_converter import BaseConverter 

59from pyTRLCConverter.reqif_identifier_store import ReqifIdentifierStore 

60from pyTRLCConverter.marko.md2reqif_renderer import Md2ReqifRenderer 

61from pyTRLCConverter.marko.gfm2reqif_renderer import Gfm2ReqifRenderer 

62from pyTRLCConverter.ret import Ret 

63from pyTRLCConverter.trlc_helper import TrlcAstWalker 

64from pyTRLCConverter.logger import log_error, log_verbose 

65from pyTRLCConverter.version import __version__ 

66 

67# pylint: disable=too-many-lines 

68 

69# Variables ******************************************************************** 

70 

71# Classes ********************************************************************** 

72 

73 

74# pylint: disable-next=too-many-instance-attributes 

75class ReqifConverter(BaseConverter): 

76 """ReqifConverter provides functionality for converting to ReqIF format.""" 

77 

78 REQIF_VERSION = "1.0" 

79 EMPTY_ATTRIBUTE_DEFAULT = "" 

80 

81 OUTPUT_FILE_NAME_DEFAULT = "output.reqif" 

82 TOP_LEVEL_DEFAULT = "Specification" 

83 

84 DATATYPE_XHTML_IDENTIFIER = "datatype-xhtml" 

85 DATATYPE_STRING_IDENTIFIER = "datatype-string" 

86 SPECIFICATION_TYPE_IDENTIFIER = "specification-type-trlc" 

87 XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" 

88 

89 SYSTEM_ATTRIBUTE_PREFIX = "ReqIF." 

90 ATTRIBUTE_KEY_RECORD_FOREIGN_ID = "foreignID" 

91 

92 def __init__(self, args: Any) -> None: 

93 # lobster-trace: SwRequirements.sw_req_reqif 

94 """ 

95 Initializes the converter. 

96 

97 Args: 

98 args (Any): The parsed program arguments. 

99 """ 

100 super().__init__(args) 

101 

102 self._out_path = args.out 

103 self._markdown_renderer_md = Markdown(renderer=Md2ReqifRenderer) 

104 self._markdown_renderer_gfm = Markdown(renderer=Gfm2ReqifRenderer, extensions=["gfm"]) 

105 self._plantuml_tmp_dir: Optional[tempfile.TemporaryDirectory] = None 

106 

107 self._id_store_path = getattr(args, "id_store", None) 

108 self._id_store: Optional[ReqifIdentifierStore] = None 

109 

110 self._spec_objects = [] 

111 self._spec_relations = [] 

112 self._root_hierarchies = [] 

113 self._hierarchy_stack = [] 

114 self._attribute_definitions_by_type = {} 

115 self._spec_object_type_info = {} 

116 self._spec_relation_type_info = {} 

117 self._pending_relations = [] 

118 self._spec_object_identifier_by_record = {} 

119 self._id_counter = 0 

120 self._document_title = ReqifConverter.TOP_LEVEL_DEFAULT 

121 self._enum_datatype_registry = {} 

122 self._last_spec_object_identifier: Optional[str] = None 

123 self._pending_hierarchy_args: Optional[tuple] = None 

124 self._spec_title_captured: bool = False 

125 self._external_files: list = [] 

126 

127 @staticmethod 

128 def get_subcommand() -> str: 

129 # lobster-trace: SwRequirements.sw_req_reqif 

130 """ 

131 Return subcommand token for this converter. 

132 

133 Returns: 

134 str: Parser subcommand token 

135 """ 

136 return "reqif" 

137 

138 @staticmethod 

139 def get_description() -> str: 

140 # lobster-trace: SwRequirements.sw_req_reqif 

141 """ 

142 Return converter description. 

143 

144 Returns: 

145 str: Converter description 

146 """ 

147 return "Convert into ReqIF format." 

148 

149 @classmethod 

150 def register(cls, args_parser: Any) -> None: 

151 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

152 # lobster-trace: SwRequirements.sw_req_reqif_single_doc_mode 

153 # lobster-trace: SwRequirements.sw_req_reqif_top_level_default 

154 # lobster-trace: SwRequirements.sw_req_reqif_top_level_custom 

155 # lobster-trace: SwRequirements.sw_req_reqif_out_file_name_default 

156 # lobster-trace: SwRequirements.sw_req_reqif_out_file_name_custom 

157 # lobster-trace: SwRequirements.sw_req_reqif_reqifz 

158 # lobster-trace: SwRequirements.sw_req_reqif_identifier_immutable 

159 """ 

160 Register converter specific argument parser. 

161 

162 Args: 

163 args_parser (Any): Argument parser 

164 """ 

165 super().register(args_parser) 

166 

167 assert BaseConverter._parser is not None 

168 

169 BaseConverter._parser.add_argument( 

170 "-e", 

171 "--empty", 

172 type=str, 

173 default=ReqifConverter.EMPTY_ATTRIBUTE_DEFAULT, 

174 required=False, 

175 help="Every attribute value which is empty will output the string " 

176 f"(default = {ReqifConverter.EMPTY_ATTRIBUTE_DEFAULT})." 

177 ) 

178 

179 BaseConverter._parser.add_argument( 

180 "-n", 

181 "--name", 

182 type=str, 

183 default=ReqifConverter.OUTPUT_FILE_NAME_DEFAULT, 

184 required=False, 

185 help="Name of the generated output file inside the output folder " 

186 f"(default = {ReqifConverter.OUTPUT_FILE_NAME_DEFAULT}) in " 

187 "case a single document is generated." 

188 ) 

189 

190 BaseConverter._parser.add_argument( 

191 "-sd", 

192 "--single-document", 

193 action="store_true", 

194 required=False, 

195 default=False, 

196 help="Generate a single document instead of multiple files. The default is to generate multiple files." 

197 ) 

198 

199 BaseConverter._parser.add_argument( 

200 "-tl", 

201 "--top-level", 

202 type=str, 

203 default=ReqifConverter.TOP_LEVEL_DEFAULT, 

204 required=False, 

205 help="Name of the top level heading, required in single document mode " 

206 f"(default = {ReqifConverter.TOP_LEVEL_DEFAULT})." 

207 ) 

208 

209 BaseConverter._parser.add_argument( 

210 "--reqifz", 

211 action="store_true", 

212 required=False, 

213 default=False, 

214 help="Archive the ReqIF output as a ZIP file with the .reqifz extension. " 

215 "The default is to write plain .reqif files." 

216 ) 

217 

218 BaseConverter._parser.add_argument( 

219 "--id-store", 

220 type=str, 

221 default=None, 

222 required=False, 

223 help="Path to a JSON file used to keep the identifiers of ReqIF Identifiable " 

224 "elements immutable across consecutive exports. On the initial conversion " 

225 "the file is created with the generated identifiers; on subsequent " 

226 "conversions the stored identifiers are reused and new elements are added." 

227 ) 

228 

229 def begin(self) -> Ret: 

230 # lobster-trace: SwRequirements.sw_req_reqif_single_doc_mode 

231 # lobster-trace: SwRequirements.sw_req_reqif_identifier_store_init 

232 # lobster-trace: SwRequirements.sw_req_reqif_identifier_store_reuse 

233 """ 

234 Begin the conversion process. 

235 

236 Returns: 

237 Ret: Status 

238 """ 

239 result = BaseConverter.begin(self) 

240 

241 if result == Ret.OK: 

242 self._empty_attribute_value = self._args.empty 

243 log_verbose(f"Empty attribute value: {self._empty_attribute_value}") 

244 

245 if self._id_store_path is not None: 

246 self._id_store = ReqifIdentifierStore() 

247 if self._id_store.load(self._id_store_path) is False: 

248 result = Ret.ERROR 

249 

250 # Temporary directory for inline PlantUML images generated while 

251 # rendering Markdown attributes. The directory is cleaned up in 

252 # finish() once all documents have been written. 

253 # pylint: disable-next=consider-using-with 

254 self._plantuml_tmp_dir = tempfile.TemporaryDirectory(prefix="pyTRLCConverter_reqif_") 

255 

256 if self._args.single_document is True: 

257 log_verbose("Single document mode.") 

258 self._reset_document_state(self._args.top_level) 

259 else: 

260 log_verbose("Multiple document mode.") 

261 

262 return result 

263 

264 def enter_file(self, file_name: str) -> Ret: 

265 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

266 """Enter a file and reset document state in multiple document mode. 

267 

268 Args: 

269 file_name (str): File name 

270 

271 Returns: 

272 Ret: Status 

273 """ 

274 if self._args.single_document is False: 

275 file_title = os.path.splitext(os.path.basename(file_name))[0] 

276 self._reset_document_state(file_title) 

277 

278 return Ret.OK 

279 

280 def leave_file(self, file_name: str) -> Ret: 

281 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

282 """Leave a file and write the ReqIF document in multiple document mode. 

283 

284 Args: 

285 file_name (str): File name 

286 

287 Returns: 

288 Ret: Status 

289 """ 

290 if self._args.single_document is False: 

291 self._flush_pending_hierarchy() 

292 out_file_name = self._file_name_trlc_to_reqif(file_name) 

293 return self._write_document(out_file_name) 

294 

295 return Ret.OK 

296 

297 def convert_section(self, section: str, level: int) -> Ret: 

298 # lobster-trace: SwRequirements.sw_req_reqif_section 

299 """Process the given section item as a named SPEC-HIERARCHY container. 

300 

301 The section reuses the last processed TRLC record's SPEC-OBJECT as the mandatory 

302 SPEC-OBJECT-REF in the SPEC-HIERARCHY node (the ReqIF v1.2 schema requires 

303 minOccurs=1). The section title is carried by the hierarchy node's LONG-NAME. 

304 No dedicated section SPEC-OBJECT is created. 

305 

306 If no record has been processed yet, the section name is used as the document 

307 title (specification long-name) for the very first such section. Any further 

308 section without a preceding record emits a warning and is skipped. 

309 

310 Args: 

311 section (str): The section name 

312 level (int): The section indentation level 

313 

314 Returns: 

315 Ret: Status 

316 """ 

317 assert len(section) > 0 

318 

319 if self._last_spec_object_identifier is None: 

320 if self._spec_title_captured is False: 

321 self._document_title = section 

322 self._spec_title_captured = True 

323 else: 

324 log_error(f"Warning: Section '{section}' has no preceding record; skipped in ReqIF hierarchy.") 

325 return Ret.OK 

326 

327 if self._pending_hierarchy_args is not None: 

328 # Promote the pending record to the section container: it is placed once in the 

329 # hierarchy at the section's level (is_container=True) instead of being flushed 

330 # as a standalone leaf. 

331 section_spec_object_id = self._pending_hierarchy_args[0] 

332 self._pending_hierarchy_args = None 

333 else: 

334 # No pending record; reuse the last known spec-object identifier. 

335 section_spec_object_id = self._last_spec_object_identifier 

336 

337 self._append_hierarchy(section_spec_object_id, level, section, is_container=True) 

338 

339 return Ret.OK 

340 

341 # pylint: disable-next=too-many-locals 

342 def convert_record_object_generic(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret: 

343 # lobster-trace: SwRequirements.sw_req_reqif_record 

344 """Convert a record object generically to a ReqIF spec-object. 

345 

346 Args: 

347 record (Record_Object): The record object. 

348 level (int): The record level. 

349 translation (Optional[dict]): Translation dictionary for the record object. 

350 If None, no translation is applied. 

351 

352 Returns: 

353 Ret: Status 

354 """ 

355 self._flush_pending_hierarchy() 

356 trlc_ast_walker = self._get_trlc_ast_walker() 

357 attribute_value_map = { 

358 ReqifConverter.ATTRIBUTE_KEY_RECORD_FOREIGN_ID: { 

359 "long_name": f"{ReqifConverter.SYSTEM_ATTRIBUTE_PREFIX}ForeignID", 

360 "value": record.name, 

361 "attribute_type": SpecObjectAttributeType.STRING 

362 } 

363 } 

364 type_key = self._get_record_type_key(record) 

365 

366 for name, value in record.field.items(): 

367 if self._queue_spec_relations_from_expression(record, value, name) is True: 

368 continue 

369 

370 attribute_name = self._translate_attribute_name(translation, name) 

371 enum_type = self._get_field_enum_type(record, name) 

372 

373 if enum_type is not None: 

374 enum_values = self._collect_enum_values_from_expression(value) 

375 if enum_values is not None: 

376 attribute_value_map[f"field_{name}"] = { 

377 "long_name": attribute_name, 

378 "value": enum_values, 

379 "attribute_type": SpecObjectAttributeType.ENUMERATION, 

380 "enum_type": enum_type, 

381 } 

382 continue 

383 

384 walker_result = trlc_ast_walker.walk(value) 

385 

386 attribute_value = "" 

387 if isinstance(walker_result, list): 

388 attribute_value = "\n".join([str(item) for item in walker_result]) 

389 else: 

390 attribute_value = str(walker_result) 

391 

392 if len(attribute_value) == 0: 

393 attribute_value = self._empty_attribute_value 

394 

395 rendered_value = self._render( 

396 package_name=record.n_package.name, 

397 type_name=record.n_typ.name, 

398 attribute_name=name, 

399 attribute_value=attribute_value 

400 ) 

401 

402 attribute_value_map[f"field_{name}"] = { 

403 "long_name": attribute_name, 

404 "value": rendered_value, 

405 "attribute_type": SpecObjectAttributeType.XHTML 

406 } 

407 

408 spec_object = self._create_spec_object( 

409 item_name=record.name, 

410 type_key=type_key, 

411 type_long_name=record.n_typ.name, 

412 attribute_value_map=attribute_value_map, 

413 identifier_key=f"spec-object:{record.n_package.name}.{record.name}" 

414 ) 

415 self._spec_object_identifier_by_record[id(record)] = spec_object.identifier 

416 self._last_spec_object_identifier = spec_object.identifier 

417 self._pending_hierarchy_args = (spec_object.identifier, level, record.name) 

418 

419 return Ret.OK 

420 

421 def finish(self) -> Ret: 

422 # lobster-trace: SwRequirements.sw_req_reqif_single_doc_mode 

423 # lobster-trace: SwRequirements.sw_req_reqif_reqifz 

424 # lobster-trace: SwRequirements.sw_req_reqif_identifier_store_init 

425 # lobster-trace: SwRequirements.sw_req_reqif_identifier_store_reuse 

426 """Finish the conversion process and write the output in single document mode. 

427 

428 The persistent identifier store, if enabled, is written back so that the 

429 generated identifiers stay immutable on subsequent conversions. 

430 

431 Returns: 

432 Ret: Status 

433 """ 

434 result = Ret.OK 

435 

436 if self._args.single_document is True: 

437 self._flush_pending_hierarchy() 

438 result = self._write_document(self._args.name) 

439 

440 if self._id_store is not None: 

441 if self._id_store.save(self._id_store_path) is False: 

442 result = Ret.ERROR 

443 

444 if self._plantuml_tmp_dir is not None: 

445 self._plantuml_tmp_dir.cleanup() 

446 self._plantuml_tmp_dir = None 

447 

448 return result 

449 

450 def _write_document(self, file_name: str) -> Ret: 

451 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

452 # lobster-trace: SwRequirements.sw_req_reqif_single_doc_mode 

453 # lobster-trace: SwRequirements.sw_req_reqif_out_file_name_default 

454 # lobster-trace: SwRequirements.sw_req_reqif_out_file_name_custom 

455 # lobster-trace: SwRequirements.sw_req_reqif_reqifz 

456 # lobster-trace: SwRequirements.sw_req_reqif_render_path 

457 """Build the ReqIF bundle and write it to the output file. 

458 

459 Without --reqifz the .reqif file is written directly into the output folder. 

460 With --reqifz a subfolder named after the document is created inside the 

461 output folder, the .reqif file is placed there, and all files inside that 

462 subfolder are bundled into a .reqifz archive in the output folder. 

463 

464 Args: 

465 file_name (str): Output file name (without path prefix). 

466 

467 Returns: 

468 Ret: Status 

469 """ 

470 doc_name = os.path.splitext(os.path.basename(file_name))[0] 

471 

472 try: 

473 reqif_xml = ReqIFUnparser.unparse(self._build_reqif_bundle()) 

474 

475 if self._args.reqifz: 

476 self._bundle_as_reqifz(doc_name, reqif_xml) 

477 else: 

478 out_file_name = os.path.join(self._out_path, file_name) if 0 < len(self._out_path) else file_name 

479 with open(out_file_name, "w", encoding="utf-8") as fd: 

480 fd.write(reqif_xml) 

481 out_dir = self._out_path if 0 < len(self._out_path) else "." 

482 self._copy_external_files(out_dir) 

483 

484 return Ret.OK 

485 

486 except (OSError, IOError, ValueError) as exc: 

487 log_error(f"Failed to write ReqIF output: {exc}") 

488 return Ret.ERROR 

489 

490 def _bundle_as_reqifz(self, doc_name: str, reqif_xml: str) -> None: 

491 # lobster-trace: SwRequirements.sw_req_reqif_reqifz 

492 # lobster-trace: SwRequirements.sw_req_reqif_render_path 

493 """Create the document subfolder, write the .reqif, and bundle into a .reqifz archive. 

494 

495 The subfolder is named after the document and placed inside the output folder. 

496 All files inside the subfolder are included in the archive with paths relative 

497 to the subfolder root. 

498 

499 Args: 

500 doc_name (str): Document base name (no extension). 

501 reqif_xml (str): Serialised ReqIF XML content. 

502 """ 

503 subfolder = os.path.join(self._out_path, doc_name) if 0 < len(self._out_path) else doc_name 

504 reqifz_path = (os.path.join(self._out_path, doc_name + ".reqifz") 

505 if 0 < len(self._out_path) else doc_name + ".reqifz") 

506 os.makedirs(subfolder, exist_ok=True) 

507 with open(os.path.join(subfolder, doc_name + ".reqif"), "w", encoding="utf-8") as fd: 

508 fd.write(reqif_xml) 

509 self._copy_external_files(subfolder) 

510 with zipfile.ZipFile(reqifz_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: 

511 for dirpath, _, filenames in os.walk(subfolder): 

512 for fname in filenames: 

513 abs_path = os.path.join(dirpath, fname) 

514 arc_name = os.path.relpath(abs_path, subfolder).replace(os.sep, "/") 

515 zf.write(abs_path, arc_name) 

516 

517 def _build_reqif_bundle(self) -> ReqIFBundle: 

518 # lobster-trace: SwRequirements.sw_req_reqif 

519 """Assemble a complete ReqIFBundle from the accumulated spec objects, hierarchies and attribute definitions. 

520 

521 Returns: 

522 ReqIFBundle: The assembled ReqIF bundle ready for unparsing. 

523 """ 

524 last_change = self._get_reqif_timestamp() 

525 

526 data_type = ReqIFDataTypeDefinitionXHTML( 

527 identifier=ReqifConverter.DATATYPE_XHTML_IDENTIFIER, 

528 last_change=last_change, 

529 long_name="XHTML" 

530 ) 

531 

532 string_data_type = ReqIFDataTypeDefinitionString( 

533 identifier=ReqifConverter.DATATYPE_STRING_IDENTIFIER, 

534 last_change=last_change, 

535 long_name="String", 

536 max_length="255" 

537 ) 

538 

539 spec_object_type_list = [ 

540 ReqIFSpecObjectType.create( 

541 identifier=type_info["identifier"], 

542 long_name=type_info["long_name"], 

543 last_change=last_change, 

544 attribute_definitions=list(self._attribute_definitions_by_type.get(type_key, {}).values()) 

545 ) 

546 for type_key, type_info in self._spec_object_type_info.items() 

547 ] 

548 

549 spec_relation_type_list = [ 

550 ReqIFSpecRelationType( 

551 identifier=relation_type_info["identifier"], 

552 last_change=last_change, 

553 long_name=relation_type_info["long_name"], 

554 is_self_closed=True, 

555 attribute_definitions=None 

556 ) 

557 for relation_type_info in self._spec_relation_type_info.values() 

558 ] 

559 

560 specification_type = ReqIFSpecificationType( 

561 identifier=ReqifConverter.SPECIFICATION_TYPE_IDENTIFIER, 

562 last_change=last_change, 

563 long_name="TRLC Specification", 

564 spec_attributes=[] 

565 ) 

566 

567 specification = ReqIFSpecification( 

568 identifier=self._document_title, 

569 long_name=self._document_title, 

570 last_change=last_change, 

571 specification_type=ReqifConverter.SPECIFICATION_TYPE_IDENTIFIER, 

572 values=[], 

573 children=self._root_hierarchies 

574 ) 

575 

576 req_if_header = ReqIFReqIFHeader( 

577 identifier=self._obtain_identifier(f"req-if-header:{self._document_title}", "req-if-header"), 

578 comment="Generated by pyTRLCConverter", 

579 creation_time=last_change, 

580 repository_id="", 

581 req_if_tool_id=f"pyTRLCConverter {__version__}", 

582 req_if_version=ReqifConverter.REQIF_VERSION, 

583 source_tool_id=f"pyTRLCConverter {__version__}", 

584 title=self._document_title 

585 ) 

586 

587 namespace_info = ReqIFNamespaceInfo.create_default() 

588 namespace_info.schema_namespace = ReqifConverter.XSI_NAMESPACE 

589 namespace_info.schema_location = ( 

590 f"{ReqIFNamespaceInfo.REQIF_XSD} {ReqIFNamespaceInfo.REQIF_XSD}" 

591 ) 

592 

593 enum_data_types = [ 

594 ReqIFDataTypeDefinitionEnumeration( 

595 identifier=info["identifier"], 

596 long_name=info["long_name"], 

597 last_change=last_change, 

598 values=info["values"] 

599 ) 

600 for info in self._enum_datatype_registry.values() 

601 ] 

602 

603 content = ReqIFReqIFContent( 

604 data_types=[data_type, string_data_type, *enum_data_types], 

605 spec_types=[*spec_object_type_list, *spec_relation_type_list, specification_type], 

606 spec_objects=self._spec_objects, 

607 spec_relations=self._build_spec_relations(), 

608 specifications=[specification] 

609 ) 

610 

611 return ReqIFBundle( 

612 namespace_info=namespace_info, 

613 req_if_header=req_if_header, 

614 core_content=ReqIFCoreContent(req_if_content=content), 

615 tool_extensions_tag_exists=False, 

616 lookup=ReqIFObjectLookup.empty(), 

617 exceptions=[] 

618 ) 

619 

620 def _reset_document_state(self, title: str) -> None: 

621 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

622 # lobster-trace: SwRequirements.sw_req_reqif_single_doc_mode 

623 """Reset all per-document state. 

624 

625 Args: 

626 title (str): The document title used as the specification long-name. 

627 """ 

628 self._spec_objects = [] 

629 self._spec_relations = [] 

630 self._root_hierarchies = [] 

631 self._hierarchy_stack = [] 

632 self._attribute_definitions_by_type = {} 

633 self._spec_object_type_info = {} 

634 self._spec_relation_type_info = {} 

635 self._pending_relations = [] 

636 self._spec_object_identifier_by_record = {} 

637 self._id_counter = 0 

638 self._document_title = title 

639 self._enum_datatype_registry = {} 

640 self._last_spec_object_identifier = None 

641 self._pending_hierarchy_args = None 

642 self._spec_title_captured = False 

643 self._external_files = [] 

644 

645 def _flush_pending_hierarchy(self) -> None: 

646 # lobster-trace: SwRequirements.sw_req_reqif_section 

647 """Flush a deferred record hierarchy node as a standalone leaf. 

648 

649 When a record is processed its hierarchy insertion is deferred so that a 

650 following section can promote it to a container instead. Call this method 

651 before inserting any new node (record or section) that must not consume the 

652 pending record. 

653 """ 

654 if self._pending_hierarchy_args is not None: 

655 pending_id, pending_level, pending_name = self._pending_hierarchy_args 

656 self._append_hierarchy(pending_id, pending_level + 1, pending_name, 

657 is_container=False) 

658 self._pending_hierarchy_args = None 

659 

660 def _append_hierarchy(self, spec_object_identifier: str, level: int, long_name: str, 

661 is_container: bool) -> None: 

662 # lobster-trace: SwRequirements.sw_req_reqif_section 

663 # lobster-trace: SwRequirements.sw_req_reqif_record 

664 """Append a new ReqIFSpecHierarchy node at the given nesting level. 

665 

666 The internal hierarchy stack is pruned so the new node is correctly 

667 attached either as a child of the current parent or as a root entry. 

668 

669 Args: 

670 spec_object_identifier (str): Identifier of the spec-object this hierarchy node references. 

671 level (int): TRLC nesting level (0 = top-level). 

672 long_name (str): Display name for the hierarchy node. 

673 is_container (bool): If True, keep the new hierarchy node on the stack as a parent. 

674 """ 

675 hierarchy_level = max(1, level + 1) 

676 

677 while (len(self._hierarchy_stack) > 0 and 

678 self._hierarchy_stack[-1].level >= hierarchy_level): 

679 self._hierarchy_stack.pop() 

680 

681 hierarchy = ReqIFSpecHierarchy( 

682 identifier=self._obtain_identifier( 

683 f"hierarchy:{spec_object_identifier}:{long_name}", "hierarchy"), 

684 spec_object=spec_object_identifier, 

685 level=hierarchy_level, 

686 children=[], 

687 long_name=long_name, 

688 ref_then_children_order=True, 

689 last_change=self._get_reqif_timestamp(), 

690 editable=False, 

691 is_table_internal=False, 

692 is_self_closed=False 

693 ) 

694 

695 if len(self._hierarchy_stack) > 0: 

696 self._hierarchy_stack[-1].add_child(hierarchy) 

697 else: 

698 self._root_hierarchies.append(hierarchy) 

699 

700 if is_container is True: 

701 self._hierarchy_stack.append(hierarchy) 

702 

703 # pylint: disable-next=too-many-arguments,too-many-positional-arguments 

704 def _create_spec_object(self, 

705 item_name: str, 

706 type_key: str, 

707 type_long_name: str, 

708 attribute_value_map: dict[str, dict[str, Any]], 

709 identifier_key: str) -> ReqIFSpecObject: 

710 # lobster-trace: SwRequirements.sw_req_reqif_record 

711 # lobster-trace: SwRequirements.sw_req_reqif_identifier_immutable 

712 """Create a ReqIFSpecObject for the given ReqIF type and field attributes. 

713 

714 Args: 

715 item_name (str): Name of the TRLC item used as the spec-object long-name. 

716 type_key (str): Internal key of the ReqIF spec-object type. 

717 type_long_name (str): Human-readable long-name of the ReqIF spec-object type. 

718 attribute_value_map (dict[str, dict[str, Any]]): Mapping from field key to 

719 ``{"long_name": ..., "value": ...}`` for each dynamic field attribute. 

720 identifier_key (str): Stable logical key for the spec-object identifier. 

721 

722 Returns: 

723 ReqIFSpecObject: The created and registered spec-object. 

724 """ 

725 self._ensure_spec_object_type(type_key, type_long_name) 

726 

727 attributes = [] 

728 

729 for key in sorted(attribute_value_map): 

730 attribute_info = attribute_value_map[key] 

731 attribute_type = attribute_info.get("attribute_type", SpecObjectAttributeType.XHTML) 

732 

733 if attribute_type == SpecObjectAttributeType.STRING: 

734 attributes.append( 

735 self._create_string_attribute( 

736 type_key, 

737 key, 

738 attribute_info["long_name"], 

739 attribute_info["value"] 

740 ) 

741 ) 

742 elif attribute_type == SpecObjectAttributeType.ENUMERATION: 

743 attributes.append( 

744 self._create_enum_attribute( 

745 type_key, 

746 key, 

747 attribute_info["long_name"], 

748 attribute_info["value"], 

749 attribute_info["enum_type"] 

750 ) 

751 ) 

752 else: 

753 attributes.append( 

754 self._create_xhtml_attribute( 

755 type_key, 

756 key, 

757 attribute_info["long_name"], 

758 attribute_info["value"] 

759 ) 

760 ) 

761 

762 spec_object = ReqIFSpecObject( 

763 identifier=self._obtain_identifier(identifier_key, "spec-object"), 

764 attributes=attributes, 

765 spec_object_type=self._get_spec_object_type_identifier(type_key), 

766 long_name=item_name, 

767 last_change=self._get_reqif_timestamp() 

768 ) 

769 

770 self._spec_objects.append(spec_object) 

771 

772 return spec_object 

773 

774 def _create_xhtml_attribute(self, type_key: str, definition_key: str, long_name: str, 

775 xhtml_value: str) -> SpecObjectAttribute: 

776 # lobster-trace: SwRequirements.sw_req_reqif_record 

777 """Create a SpecObjectAttribute of type XHTML, ensuring the corresponding attribute definition exists. 

778 

779 Args: 

780 type_key (str): Internal key of the owning ReqIF spec-object type. 

781 definition_key (str): Internal dictionary key used to look up or create the attribute definition. 

782 long_name (str): Human-readable attribute name registered in the spec-object type. 

783 xhtml_value (str): XHTML-wrapped content string. 

784 

785 Returns: 

786 SpecObjectAttribute: The created attribute. 

787 """ 

788 definition_identifier = self._ensure_attribute_definition( 

789 type_key, 

790 definition_key, 

791 long_name, 

792 SpecObjectAttributeType.XHTML 

793 ) 

794 

795 return SpecObjectAttribute( 

796 attribute_type=SpecObjectAttributeType.XHTML, 

797 definition_ref=definition_identifier, 

798 value=xhtml_value 

799 ) 

800 

801 def _create_string_attribute(self, type_key: str, definition_key: str, long_name: str, 

802 value: str) -> SpecObjectAttribute: 

803 # lobster-trace: SwRequirements.sw_req_reqif_record 

804 """Create a SpecObjectAttribute of type STRING for the given ReqIF spec-object type. 

805 

806 Args: 

807 type_key (str): Internal key of the owning ReqIF spec-object type. 

808 definition_key (str): Internal dictionary key used to look up or create the attribute definition. 

809 long_name (str): Human-readable attribute name registered in the spec-object type. 

810 value (str): String attribute value. 

811 

812 Returns: 

813 SpecObjectAttribute: The created attribute. 

814 """ 

815 definition_identifier = self._ensure_attribute_definition( 

816 type_key, 

817 definition_key, 

818 long_name, 

819 SpecObjectAttributeType.STRING 

820 ) 

821 

822 return SpecObjectAttribute( 

823 attribute_type=SpecObjectAttributeType.STRING, 

824 definition_ref=definition_identifier, 

825 value=value 

826 ) 

827 

828 # pylint: disable-next=too-many-arguments,too-many-positional-arguments 

829 def _create_enum_attribute(self, type_key: str, definition_key: str, long_name: str, 

830 literal_names: list, enum_type: Enumeration_Type) -> SpecObjectAttribute: 

831 # lobster-trace: SwRequirements.sw_req_reqif_enum 

832 """Create a SpecObjectAttribute of type ENUMERATION for the given ReqIF spec-object type. 

833 

834 Args: 

835 type_key (str): Internal key of the owning ReqIF spec-object type. 

836 definition_key (str): Internal dictionary key used to look up or create the attribute definition. 

837 long_name (str): Human-readable attribute name registered in the spec-object type. 

838 literal_names (list): List of TRLC enumeration literal names for the attribute value. 

839 enum_type (Enumeration_Type): The TRLC enumeration type. 

840 

841 Returns: 

842 SpecObjectAttribute: The created attribute. 

843 """ 

844 definition_identifier = self._ensure_enum_attribute_definition( 

845 type_key, definition_key, long_name, enum_type 

846 ) 

847 registry = self._enum_datatype_registry[enum_type.name] 

848 value_identifiers = [registry["literal_identifier_by_name"][name] for name in literal_names] 

849 

850 return SpecObjectAttribute( 

851 attribute_type=SpecObjectAttributeType.ENUMERATION, 

852 definition_ref=definition_identifier, 

853 value=value_identifiers 

854 ) 

855 

856 def _ensure_enum_datatype(self, enum_type: Enumeration_Type) -> str: 

857 # lobster-trace: SwRequirements.sw_req_reqif_enum 

858 # lobster-trace: SwRequirements.sw_req_reqif_enum_key_order 

859 """Return the identifier of an existing DATATYPE-DEFINITION-ENUMERATION, or create and register a new one. 

860 

861 ENUM-VALUE keys are assigned as consecutive integers starting at 0, 

862 in the order the literals are declared in the RSL enumeration. 

863 

864 Args: 

865 enum_type (Enumeration_Type): The TRLC enumeration type. 

866 

867 Returns: 

868 str: The datatype definition identifier. 

869 """ 

870 if enum_type.name in self._enum_datatype_registry: 

871 return self._enum_datatype_registry[enum_type.name]["identifier"] 

872 

873 last_change = self._get_reqif_timestamp() 

874 sanitized = self._sanitize_identifier_token(enum_type.name) 

875 datatype_identifier = f"datatype-enum-{sanitized}" 

876 

877 enum_values = [] 

878 literal_identifier_by_name = {} 

879 for key_idx, literal_spec in enumerate(enum_type.literals.table.values()): 

880 value_identifier = f"{datatype_identifier}-value-{key_idx}" 

881 enum_values.append(ReqIFEnumValue( 

882 identifier=value_identifier, 

883 key=str(key_idx), 

884 long_name=literal_spec.name, 

885 last_change=last_change, 

886 other_content="" 

887 )) 

888 literal_identifier_by_name[literal_spec.name] = value_identifier 

889 

890 self._enum_datatype_registry[enum_type.name] = { 

891 "identifier": datatype_identifier, 

892 "long_name": enum_type.name, 

893 "values": enum_values, 

894 "literal_identifier_by_name": literal_identifier_by_name 

895 } 

896 

897 return datatype_identifier 

898 

899 def _ensure_enum_attribute_definition(self, type_key: str, definition_key: str, 

900 long_name: str, enum_type: Enumeration_Type) -> str: 

901 # lobster-trace: SwRequirements.sw_req_reqif_enum 

902 """Return the identifier of an existing ATTRIBUTE-DEFINITION-ENUMERATION, or create and register a new one. 

903 

904 Args: 

905 type_key (str): Internal key of the owning ReqIF spec-object type. 

906 definition_key (str): Internal dictionary key for the attribute definition. 

907 long_name (str): Human-readable name to assign when creating a new definition. 

908 enum_type (Enumeration_Type): The TRLC enumeration type. 

909 

910 Returns: 

911 str: The attribute definition identifier. 

912 """ 

913 attribute_definitions = self._attribute_definitions_by_type.setdefault(type_key, {}) 

914 

915 if definition_key in attribute_definitions: 

916 return attribute_definitions[definition_key].identifier 

917 

918 type_identifier = self._ensure_spec_object_type(type_key, type_key) 

919 sanitized_name = self._sanitize_identifier_token(definition_key) 

920 definition_identifier = f"attribute-{type_identifier}-{sanitized_name}" 

921 enum_datatype_identifier = self._ensure_enum_datatype(enum_type) 

922 

923 definition = SpecAttributeDefinition( 

924 attribute_type=SpecObjectAttributeType.ENUMERATION, 

925 identifier=definition_identifier, 

926 datatype_definition=enum_datatype_identifier, 

927 long_name=long_name, 

928 last_change=self._get_reqif_timestamp(), 

929 multi_valued=False 

930 ) 

931 

932 attribute_definitions[definition_key] = definition 

933 

934 return definition_identifier 

935 

936 @staticmethod 

937 def _get_field_enum_type(record: Record_Object, field_name: str) -> Optional[Enumeration_Type]: 

938 # lobster-trace: SwRequirements.sw_req_reqif_enum 

939 # lobster-trace: SwRequirements.sw_req_reqif_enum_null 

940 """Return the TRLC Enumeration_Type for the named field, or None if it is not an enum field. 

941 

942 Traverses the component hierarchy including inherited components. 

943 

944 Args: 

945 record (Record_Object): The TRLC record object. 

946 field_name (str): The field name to look up. 

947 

948 Returns: 

949 Optional[Enumeration_Type]: The enumeration type, or None. 

950 """ 

951 simplified = Symbol_Table.simplified_name(field_name) 

952 stab = getattr(record.n_typ, "components", None) 

953 component = None 

954 

955 while stab is not None and component is None: 

956 component = stab.table.get(simplified) 

957 stab = stab.parent 

958 

959 if component is not None and isinstance(component.n_typ, Enumeration_Type): 

960 return component.n_typ 

961 

962 return None 

963 

964 @staticmethod 

965 def _collect_enum_values_from_expression(value: Expression) -> Optional[list]: 

966 # lobster-trace: SwRequirements.sw_req_reqif_enum 

967 # lobster-trace: SwRequirements.sw_req_reqif_enum_null 

968 """Collect TRLC enumeration literal names from a field expression. 

969 

970 Args: 

971 value (Expression): The TRLC field expression. 

972 

973 Returns: 

974 Optional[list]: List of literal name strings, or None if the expression is null or unsupported. 

975 """ 

976 if isinstance(value, Enumeration_Literal): 

977 return [value.value.name] 

978 

979 if isinstance(value, Array_Aggregate): 

980 names = [item.value.name for item in value.value if isinstance(item, Enumeration_Literal)] 

981 return names if names else None 

982 

983 return None 

984 

985 def _ensure_spec_object_type(self, type_key: str, long_name: str) -> str: 

986 # lobster-trace: SwRequirements.sw_req_reqif_record 

987 """Return the identifier of an existing ReqIF spec-object type, or create and register a new one. 

988 

989 Args: 

990 type_key (str): Internal dictionary key for the spec-object type. 

991 long_name (str): Human-readable name to assign when creating a new type. 

992 

993 Returns: 

994 str: The spec-object type identifier. 

995 """ 

996 if type_key in self._spec_object_type_info: 

997 return self._spec_object_type_info[type_key]["identifier"] 

998 

999 sanitized_name = self._sanitize_identifier_token(type_key) 

1000 type_identifier = f"spec-object-type-{sanitized_name}" 

1001 

1002 self._spec_object_type_info[type_key] = { 

1003 "identifier": type_identifier, 

1004 "long_name": long_name 

1005 } 

1006 self._attribute_definitions_by_type[type_key] = {} 

1007 

1008 return type_identifier 

1009 

1010 def _get_spec_object_type_identifier(self, type_key: str) -> str: 

1011 # lobster-trace: SwRequirements.sw_req_reqif_record 

1012 """Return the identifier of the requested ReqIF spec-object type. 

1013 

1014 Args: 

1015 type_key (str): Internal dictionary key for the spec-object type. 

1016 

1017 Returns: 

1018 str: The spec-object type identifier. 

1019 """ 

1020 type_info = self._spec_object_type_info.get(type_key) 

1021 assert type_info is not None 

1022 return type_info["identifier"] 

1023 

1024 def _ensure_attribute_definition(self, type_key: str, definition_key: str, long_name: str, 

1025 attribute_type: SpecObjectAttributeType) -> str: 

1026 # lobster-trace: SwRequirements.sw_req_reqif_record 

1027 """Return the identifier of an existing attribute definition, or create and register a new one. 

1028 

1029 Args: 

1030 type_key (str): Internal key of the owning ReqIF spec-object type. 

1031 definition_key (str): Internal dictionary key for the attribute definition. 

1032 long_name (str): Human-readable name to assign when creating a new definition. 

1033 attribute_type (SpecObjectAttributeType): ReqIF attribute type. 

1034 

1035 Returns: 

1036 str: The attribute definition identifier. 

1037 """ 

1038 attribute_definitions = self._attribute_definitions_by_type.setdefault(type_key, {}) 

1039 

1040 if definition_key in attribute_definitions: 

1041 definition = attribute_definitions[definition_key] 

1042 assert isinstance(definition, SpecAttributeDefinition) 

1043 return definition.identifier 

1044 

1045 type_identifier = self._ensure_spec_object_type(type_key, type_key) 

1046 sanitized_name = self._sanitize_identifier_token(definition_key) 

1047 definition_identifier = f"attribute-{type_identifier}-{sanitized_name}" 

1048 

1049 definition = SpecAttributeDefinition( 

1050 attribute_type=attribute_type, 

1051 identifier=definition_identifier, 

1052 datatype_definition=self._get_datatype_identifier(attribute_type), 

1053 long_name=long_name, 

1054 last_change=self._get_reqif_timestamp(), 

1055 multi_valued=None 

1056 ) 

1057 

1058 attribute_definitions[definition_key] = definition 

1059 

1060 return definition_identifier 

1061 

1062 def _get_record_type_key(self, record: Record_Object) -> str: 

1063 # lobster-trace: SwRequirements.sw_req_reqif_record 

1064 """Return the ReqIF type key for the given TRLC record. 

1065 

1066 Args: 

1067 record (Record_Object): The record object. 

1068 

1069 Returns: 

1070 str: ReqIF type key derived from the TRLC record type. 

1071 """ 

1072 return record.n_typ.name 

1073 

1074 def _queue_spec_relation(self, source_record: Record_Object, record_reference: Record_Reference, 

1075 relation_name: str) -> None: 

1076 # lobster-trace: SwRequirements.sw_req_reqif_relation 

1077 """Queue a ReqIF spec relation derived from a TRLC record reference. 

1078 

1079 Args: 

1080 source_record (Record_Object): Source record containing the reference. 

1081 record_reference (Record_Reference): Referenced target record. 

1082 relation_name (str): TRLC field name that defines the relation type. 

1083 """ 

1084 assert record_reference.target is not None 

1085 

1086 self._ensure_spec_relation_type(relation_name, relation_name) 

1087 self._pending_relations.append( 

1088 { 

1089 "source_record": id(source_record), 

1090 "target_record": id(record_reference.target), 

1091 "relation_type_key": relation_name, 

1092 "long_name": relation_name 

1093 } 

1094 ) 

1095 

1096 def _queue_spec_relations_from_expression(self, source_record: Record_Object, expression: Expression, 

1097 relation_name: str) -> bool: 

1098 # lobster-trace: SwRequirements.sw_req_reqif_relation 

1099 """Queue ReqIF spec relations from a TRLC expression if it consists of record references. 

1100 

1101 Args: 

1102 source_record (Record_Object): Source record containing the reference expression. 

1103 expression (Expression): TRLC field expression to inspect. 

1104 relation_name (str): TRLC field name that defines the relation type. 

1105 

1106 Returns: 

1107 bool: True if the expression was handled as one or more record references. 

1108 """ 

1109 if isinstance(expression, Record_Reference): 

1110 self._queue_spec_relation(source_record, expression, relation_name) 

1111 return True 

1112 

1113 if isinstance(expression, Array_Aggregate): 

1114 has_record_references = False 

1115 

1116 for item in expression.value: 

1117 item_is_relation = self._queue_spec_relations_from_expression( 

1118 source_record, 

1119 item, 

1120 relation_name 

1121 ) 

1122 has_record_references = has_record_references or item_is_relation 

1123 

1124 return has_record_references 

1125 

1126 return False 

1127 

1128 def _ensure_spec_relation_type(self, relation_type_key: str, long_name: str) -> str: 

1129 # lobster-trace: SwRequirements.sw_req_reqif_relation 

1130 """Return the identifier of an existing ReqIF spec-relation type, or create and register a new one. 

1131 

1132 Args: 

1133 relation_type_key (str): Internal dictionary key for the spec-relation type. 

1134 long_name (str): Human-readable relation type name. 

1135 

1136 Returns: 

1137 str: The spec-relation type identifier. 

1138 """ 

1139 if relation_type_key in self._spec_relation_type_info: 

1140 return self._spec_relation_type_info[relation_type_key]["identifier"] 

1141 

1142 relation_type_identifier = f"spec-relation-type-{self._sanitize_identifier_token(relation_type_key)}" 

1143 self._spec_relation_type_info[relation_type_key] = { 

1144 "identifier": relation_type_identifier, 

1145 "long_name": long_name 

1146 } 

1147 

1148 return relation_type_identifier 

1149 

1150 def _build_spec_relations(self) -> list[ReqIFSpecRelation]: 

1151 # lobster-trace: SwRequirements.sw_req_reqif_relation 

1152 """Build ReqIF spec relations from queued TRLC record references. 

1153 

1154 Returns: 

1155 list[ReqIFSpecRelation]: Resolved ReqIF spec relations. 

1156 """ 

1157 self._spec_relations = [] 

1158 

1159 for pending_relation in self._pending_relations: 

1160 source_identifier = self._spec_object_identifier_by_record.get(pending_relation["source_record"]) 

1161 target_identifier = self._spec_object_identifier_by_record.get(pending_relation["target_record"]) 

1162 

1163 if source_identifier is None or target_identifier is None: 

1164 log_error( 

1165 "Failed to create ReqIF relation because the source or target record is not part of the output.", 

1166 False 

1167 ) 

1168 continue 

1169 

1170 relation_type_identifier = self._ensure_spec_relation_type( 

1171 pending_relation["relation_type_key"], 

1172 pending_relation["long_name"] 

1173 ) 

1174 

1175 self._spec_relations.append( 

1176 ReqIFSpecRelation( 

1177 identifier=self._obtain_identifier( 

1178 f"spec-relation:{source_identifier}:{relation_type_identifier}:{target_identifier}", 

1179 "spec-relation"), 

1180 relation_type_ref=relation_type_identifier, 

1181 source=source_identifier, 

1182 target=target_identifier, 

1183 last_change=self._get_reqif_timestamp(), 

1184 long_name=pending_relation["long_name"] 

1185 ) 

1186 ) 

1187 

1188 return self._spec_relations 

1189 

1190 @staticmethod 

1191 def _get_datatype_identifier(attribute_type: SpecObjectAttributeType) -> str: 

1192 # lobster-trace: SwRequirements.sw_req_reqif 

1193 """Return the datatype definition identifier for the given ReqIF attribute type. 

1194 

1195 Args: 

1196 attribute_type (SpecObjectAttributeType): ReqIF attribute type. 

1197 

1198 Returns: 

1199 str: Matching datatype definition identifier. 

1200 """ 

1201 if attribute_type == SpecObjectAttributeType.STRING: 

1202 return ReqifConverter.DATATYPE_STRING_IDENTIFIER 

1203 

1204 if attribute_type == SpecObjectAttributeType.XHTML: 

1205 return ReqifConverter.DATATYPE_XHTML_IDENTIFIER 

1206 

1207 raise NotImplementedError(attribute_type) 

1208 

1209 @staticmethod 

1210 def _sanitize_identifier_token(value: str) -> str: 

1211 # lobster-trace: SwRequirements.sw_req_reqif 

1212 """Sanitize an identifier token for stable ReqIF identifiers. 

1213 

1214 Args: 

1215 value (str): Raw identifier token. 

1216 

1217 Returns: 

1218 str: Sanitized lowercase token. 

1219 """ 

1220 sanitized_value = re.sub(r"[^a-zA-Z0-9]+", "-", value.lower()).strip("-") 

1221 return sanitized_value if len(sanitized_value) > 0 else "item" 

1222 

1223 def _file_name_trlc_to_reqif(self, file_name_trlc: str) -> str: 

1224 # lobster-trace: SwRequirements.sw_req_reqif_multiple_doc_mode 

1225 """Convert a TRLC file name to a ReqIF file name. 

1226 

1227 Args: 

1228 file_name_trlc (str): TRLC file name 

1229 

1230 Returns: 

1231 str: ReqIF file name 

1232 """ 

1233 file_name = os.path.basename(file_name_trlc) 

1234 file_name = os.path.splitext(file_name)[0] + ".reqif" 

1235 

1236 return file_name 

1237 

1238 def _on_implict_null(self, _: Implicit_Null) -> str: 

1239 # lobster-trace: SwRequirements.sw_req_reqif_record 

1240 """ 

1241 Process the given implicit null value. 

1242 

1243 Returns: 

1244 str: The configured empty attribute value. 

1245 """ 

1246 return self._empty_attribute_value 

1247 

1248 def _on_record_reference(self, record_reference: Record_Reference) -> str: 

1249 # lobster-trace: SwRequirements.sw_req_reqif_record 

1250 """ 

1251 Process the given record reference value. 

1252 

1253 Args: 

1254 record_reference (Record_Reference): The record reference value. 

1255 

1256 Returns: 

1257 str: String representation of the referenced record. 

1258 """ 

1259 return str(record_reference.to_python_object()) 

1260 

1261 def _on_string_literal(self, string_literal: String_Literal) -> str: 

1262 # lobster-trace: SwRequirements.sw_req_reqif_record 

1263 """ 

1264 Process the given string literal value. 

1265 

1266 Args: 

1267 string_literal (String_Literal): The string literal value. 

1268 

1269 Returns: 

1270 str: The string literal value. 

1271 """ 

1272 return string_literal.to_string() 

1273 

1274 def _other_dispatcher(self, expression: Expression) -> str: 

1275 # lobster-trace: SwRequirements.sw_req_reqif_record 

1276 """ 

1277 Dispatcher for all other expressions. 

1278 

1279 Args: 

1280 expression (Expression): The expression to process. 

1281 

1282 Returns: 

1283 str: The processed expression. 

1284 """ 

1285 return expression.to_string() 

1286 

1287 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

1288 # lobster-trace: SwRequirements.sw_req_reqif_record 

1289 """ 

1290 Create and configure a TrlcAstWalker for traversing TRLC record field values. 

1291 

1292 Returns: 

1293 TrlcAstWalker: The configured TRLC AST walker. 

1294 """ 

1295 trlc_ast_walker = TrlcAstWalker() 

1296 trlc_ast_walker.add_dispatcher( 

1297 Implicit_Null, 

1298 None, 

1299 self._on_implict_null, 

1300 None 

1301 ) 

1302 trlc_ast_walker.add_dispatcher( 

1303 Record_Reference, 

1304 None, 

1305 self._on_record_reference, 

1306 None 

1307 ) 

1308 trlc_ast_walker.add_dispatcher( 

1309 String_Literal, 

1310 None, 

1311 self._on_string_literal, 

1312 None 

1313 ) 

1314 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher) 

1315 

1316 return trlc_ast_walker 

1317 

1318 def _render(self, package_name: str, type_name: str, attribute_name: str, attribute_value: str) -> str: 

1319 # lobster-trace: SwRequirements.sw_req_reqif_render_md 

1320 # lobster-trace: SwRequirements.sw_req_reqif_render_gfm 

1321 # lobster-trace: SwRequirements.sw_req_reqif_render_xhtml 

1322 # lobster-trace: SwRequirements.sw_req_reqif_render_path 

1323 # lobster-trace: SwRequirements.sw_req_reqif_render_table_options 

1324 """Render an attribute value to an XHTML-wrapped string based on the render configuration. 

1325 

1326 If the attribute is configured as CommonMark Markdown, it is converted via marko. 

1327 If configured as GFM, it is converted with the GFM extension. 

1328 If configured as XHTML, the value is passed through as-is inside the XHTML wrapper. 

1329 If configured as path, the value is treated as a file path and converted to an XHTML 

1330 ``<object>`` element; the file is scheduled for copying to the output folder. 

1331 Otherwise plain text is HTML-escaped and wrapped. 

1332 

1333 For Markdown and GFM formats, optional table rendering options (``border``, 

1334 ``headingStyle``) from the render configuration are applied to the generated HTML. 

1335 

1336 Args: 

1337 package_name (str): TRLC package name of the record. 

1338 type_name (str): TRLC type name of the record. 

1339 attribute_name (str): Attribute field name. 

1340 attribute_value (str): Raw attribute value string. 

1341 

1342 Returns: 

1343 str: XHTML-wrapped content string. 

1344 """ 

1345 if self._render_cfg.is_format_md(package_name, type_name, attribute_name) is True: 

1346 table_options = self._render_cfg.get_table_options(package_name, type_name, attribute_name) 

1347 return self._markdown_to_xhtml(attribute_value, gfm_mode=False, table_options=table_options) 

1348 

1349 if self._render_cfg.is_format_gfm(package_name, type_name, attribute_name) is True: 

1350 table_options = self._render_cfg.get_table_options(package_name, type_name, attribute_name) 

1351 return self._markdown_to_xhtml(attribute_value, gfm_mode=True, table_options=table_options) 

1352 

1353 if self._render_cfg.is_format_xhtml(package_name, type_name, attribute_name) is True: 

1354 if "<" in attribute_value: 

1355 return self._wrap_xhtml(attribute_value) 

1356 return self._plain_text_to_xhtml(attribute_value) 

1357 

1358 if self._render_cfg.is_format_path(package_name, type_name, attribute_name) is True: 

1359 return self._path_to_xhtml(attribute_value) 

1360 

1361 return self._plain_text_to_xhtml(attribute_value) 

1362 

1363 def _plain_text_to_xhtml(self, plain_text: str) -> str: 

1364 # lobster-trace: SwRequirements.sw_req_reqif_record 

1365 """Convert plain text to an XHTML-wrapped string. 

1366 

1367 Blank-line-separated paragraphs are wrapped in ``<p>`` tags; single 

1368 newlines within a paragraph become ``<br/>`` elements. 

1369 

1370 Args: 

1371 plain_text (str): Plain text to convert. 

1372 

1373 Returns: 

1374 str: XHTML-wrapped content string. 

1375 """ 

1376 escaped = html.escape(plain_text) 

1377 

1378 paragraph_list = [] 

1379 

1380 for paragraph in escaped.split("\n\n"): 

1381 paragraph_linebreaks = paragraph.replace("\n", "<br/>") 

1382 paragraph_list.append(f"<p>{paragraph_linebreaks}</p>") 

1383 

1384 return self._wrap_xhtml("".join(paragraph_list)) 

1385 

1386 def _markdown_to_xhtml(self, markdown_text: str, gfm_mode: bool, 

1387 table_options: Optional[dict] = None) -> str: 

1388 # lobster-trace: SwRequirements.sw_req_reqif_render_md 

1389 # lobster-trace: SwRequirements.sw_req_reqif_render_gfm 

1390 # lobster-trace: SwRequirements.sw_req_reqif_render_table_options 

1391 # lobster-trace: SwRequirements.sw_req_reqif_render_plantuml 

1392 """Convert Markdown text to an XHTML-wrapped string using marko. 

1393 

1394 If ``table_options`` is provided and non-empty, table styling is applied to the 

1395 generated HTML before wrapping (see :meth:`_apply_table_options`). 

1396 

1397 Fenced code blocks tagged ``plantuml`` are rendered as embedded SVG 

1398 images via :class:`Md2ReqifRenderer` / :class:`Gfm2ReqifRenderer`; the 

1399 generated images are registered in ``self._external_files`` so they are 

1400 copied next to the ReqIF document. 

1401 

1402 Args: 

1403 markdown_text (str): Markdown source text. 

1404 gfm_mode (bool): If True, use the GFM extension; otherwise use CommonMark. 

1405 table_options (Optional[dict]): Optional table rendering options with keys 

1406 ``"border"`` and/or ``"headingStyle"``. 

1407 

1408 Returns: 

1409 str: XHTML-wrapped HTML string. 

1410 """ 

1411 renderer = self._markdown_renderer_gfm if gfm_mode else self._markdown_renderer_md 

1412 

1413 # Wire the inline-PlantUML renderer state to this converter so generated 

1414 # SVGs are written to the converter's temp directory and tracked in 

1415 # _external_files for copying. 

1416 assert self._plantuml_tmp_dir is not None 

1417 Md2ReqifRenderer.image_dir = self._plantuml_tmp_dir.name 

1418 Md2ReqifRenderer.external_files = self._external_files 

1419 

1420 html_text = renderer.convert(markdown_text).strip() 

1421 

1422 if len(html_text) == 0: 

1423 html_text = "<p></p>" 

1424 

1425 # Marko's GFM renderer outputs HTML void elements (e.g. <input ...>) for task list 

1426 # checkboxes. XHTML requires all void elements to be self-closed (<input ... />). 

1427 html_text = re.sub(r'<(input)(\s[^>]*)>', r'<\1\2/>', html_text) 

1428 

1429 if table_options: 

1430 html_text = self._apply_table_options(html_text, table_options) 

1431 

1432 return self._wrap_xhtml(html_text) 

1433 

1434 @staticmethod 

1435 def _apply_table_options(html_text: str, table_options: dict) -> str: 

1436 # lobster-trace: SwRequirements.sw_req_reqif_render_table_options 

1437 """Apply table rendering options to generated HTML. 

1438 

1439 Replaces the plain ``<table>`` tag with one carrying a ``style`` attribute when 

1440 ``"border"`` is set, and injects a ``style`` attribute into every ``<th>`` tag 

1441 when ``"headingStyle"`` is set. Values are HTML-escaped before insertion. 

1442 

1443 Args: 

1444 html_text (str): HTML string produced by the Markdown renderer. 

1445 table_options (dict): Table options with optional keys ``"border"`` (str) and 

1446 ``"headingStyle"`` (str), both interpreted as CSS style values. 

1447 

1448 Returns: 

1449 str: HTML string with table styling applied. 

1450 """ 

1451 result = html_text 

1452 

1453 table_border = table_options.get("border", "") 

1454 if table_border: 

1455 escaped = html.escape(table_border, quote=True) 

1456 result = result.replace("<table>", f'<table style="{escaped}">') 

1457 

1458 heading_style = table_options.get("headingStyle", "") 

1459 if heading_style: 

1460 escaped = html.escape(heading_style, quote=True) 

1461 result = re.sub( 

1462 r'<th((?:\s+align="[^"]*")?)>', 

1463 lambda m: f'<th{m.group(1)} style="{escaped}">', 

1464 result 

1465 ) 

1466 

1467 return result 

1468 

1469 def _path_to_xhtml(self, file_path: str) -> str: 

1470 # lobster-trace: SwRequirements.sw_req_reqif_render_path 

1471 """Convert a file path to a ReqIF XHTML ``<object>`` element and schedule the file for copying. 

1472 

1473 The file is referenced locally by its basename. The MIME type is determined from 

1474 the file extension; if unknown, ``application/octet-stream`` is used. If ``file_path`` 

1475 is empty the value is rendered as plain text instead. 

1476 

1477 Args: 

1478 file_path (str): Path to the external file (may be relative or absolute). 

1479 

1480 Returns: 

1481 str: XHTML-wrapped ``<object>`` element string. 

1482 """ 

1483 if len(file_path) == 0: 

1484 return self._plain_text_to_xhtml(file_path) 

1485 

1486 local_name = os.path.basename(file_path) 

1487 mime_type, _ = mimetypes.guess_type(file_path) 

1488 if mime_type is None: 

1489 mime_type = "application/octet-stream" 

1490 

1491 self._external_files.append((file_path, local_name)) 

1492 

1493 return self._wrap_xhtml( 

1494 f'<object type="{html.escape(mime_type)}" data="{html.escape(local_name)}"></object>' 

1495 ) 

1496 

1497 def _copy_external_files(self, dest_dir: str) -> None: 

1498 # lobster-trace: SwRequirements.sw_req_reqif_render_path 

1499 """Copy all collected external files to the given destination directory. 

1500 

1501 Each file is copied using its basename as the destination file name. Duplicate 

1502 source paths are copied only once. If a source file cannot be read, an error 

1503 is logged and the file is skipped. 

1504 

1505 Args: 

1506 dest_dir (str): Destination directory path. 

1507 """ 

1508 copied_sources = set() 

1509 

1510 for source_path, local_name in self._external_files: 

1511 if source_path in copied_sources: 

1512 continue 

1513 

1514 dest_path = os.path.join(dest_dir, local_name) 

1515 

1516 try: 

1517 shutil.copy2(source_path, dest_path) 

1518 copied_sources.add(source_path) 

1519 except (OSError, IOError) as exc: 

1520 log_error(f"Failed to copy external file '{source_path}': {exc}", False) 

1521 

1522 @staticmethod 

1523 def _wrap_xhtml(fragment: str) -> str: 

1524 # lobster-trace: SwRequirements.sw_req_reqif 

1525 """Wrap an HTML fragment in a ReqIF-compatible XHTML div element. 

1526 

1527 Args: 

1528 fragment (str): Inner HTML fragment. 

1529 

1530 Returns: 

1531 str: XHTML-wrapped string. 

1532 """ 

1533 return "<div xmlns=\"http://www.w3.org/1999/xhtml\">" + fragment + "</div>" 

1534 

1535 @staticmethod 

1536 def _get_reqif_timestamp() -> str: 

1537 # lobster-trace: SwRequirements.sw_req_reqif 

1538 """Return the current UTC time as a ReqIF-compatible ISO 8601 timestamp. 

1539 

1540 Returns: 

1541 str: ISO 8601 timestamp string (UTC, no microseconds). 

1542 """ 

1543 return datetime.now(timezone.utc).replace(microsecond=0).isoformat() 

1544 

1545 def _new_identifier(self, prefix: str) -> str: 

1546 # lobster-trace: SwRequirements.sw_req_reqif 

1547 """Generate a unique identifier by combining the given prefix with an auto-incrementing counter. 

1548 

1549 Args: 

1550 prefix (str): Identifier prefix (e.g. ``"spec-object"`` or ``"hierarchy"``) 

1551 

1552 Returns: 

1553 str: Unique identifier string. 

1554 """ 

1555 self._id_counter += 1 

1556 return f"{prefix}-{self._id_counter}" 

1557 

1558 def _obtain_identifier(self, key: str, prefix: str) -> str: 

1559 # lobster-trace: SwRequirements.sw_req_reqif_identifier_immutable 

1560 """Obtain an identifier for an Identifiable element, persistent if a store is enabled. 

1561 

1562 When an identifier store is enabled (``--id-store``) the identifier is looked up 

1563 by its stable logical key and reused if known, keeping it immutable across 

1564 consecutive exports. Otherwise a volatile auto-incremented identifier is used. 

1565 

1566 Args: 

1567 key (str): Stable logical key identifying the ReqIF element. 

1568 prefix (str): Identifier prefix (e.g. ``"spec-object"`` or ``"hierarchy"``). 

1569 

1570 Returns: 

1571 str: The identifier associated with the element. 

1572 """ 

1573 if self._id_store is not None: 

1574 identifier = self._id_store.get_or_create(key, prefix) 

1575 else: 

1576 identifier = self._new_identifier(prefix) 

1577 

1578 return identifier