Skip to content

Commit 62473a9

Browse files
authored
Merge pull request #2621 from strictdoc-project/stanislaw/grammar_from_file
feat(sdoc|project_config): extend DOCUMENT_FROM_FILE to support grammar aliases
2 parents 97b4a38 + 00d033f commit 62473a9

28 files changed

Lines changed: 271 additions & 55 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
**/.sandbox/**
1414
__pycache__
1515

16+
/tests/unit_server/**/output/
17+
1618
### LIT/FileCheck integration tests' artifacts. ###
1719
/tests/integration/**/Output/**
1820
/tests/integration/**/sandbox/

.link_health.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ exceptions:
3434
- url: https://github.com/tiangolo/fastapi/blob/master/LICENSE
3535
- url: https://github.com/strictdoc-project/strictdoc/blob/main/about/2025_ESA_SW_Product_Assurance_Workshop.pdf
3636
- url: https://github.com/strictdoc-project/strictdoc/blob/main/strictdoc/core/project_config.py
37+
- url: https://github.com/strictdoc-project/strictdoc/blob/main/strictdoc_config.py

docs/strictdoc_01_user_guide.sdoc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,47 @@ When a ``[GRAMMAR]`` is declared with an ``IMPORT_FROM_FILE`` line, the grammar
21932193
Editing of the grammars defined in ``.sgra`` files can be only done with a text editor, it is not implemented yet in the editable web interface.
21942194
<<<
21952195

2196+
[[SECTION]]
2197+
MID: 00d5670e058d43b1b293da87e22c3a26
2198+
TITLE: Grammar aliases
2199+
2200+
[TEXT]
2201+
MID: eba1c3f3a9364f1c8785c30c6fa5f3cc
2202+
STATEMENT: >>>
2203+
To support sharing a grammar across multiple documents, possibly located in different directories, a grammar alias can be used as follows:
2204+
2205+
.. code-block: strictdoc
2206+
2207+
[DOCUMENT]
2208+
TITLE: Hello world
2209+
2210+
[GRAMMAR]
2211+
IMPORT_FROM_FILE: @my_grammar
2212+
2213+
The grammar alias itself must be registered in the project config:
2214+
2215+
.. code-block:: python
2216+
2217+
from strictdoc.core.project_config import ProjectConfig
2218+
2219+
2220+
def create_config() -> ProjectConfig:
2221+
config = ProjectConfig(
2222+
project_title="StrictDoc Documentation",
2223+
project_features=[
2224+
# Intentionally nothing.
2225+
],
2226+
grammars={
2227+
"@my_grammar": "grammar.sgra",
2228+
},
2229+
)
2230+
return config
2231+
2232+
The grammar path must be specified relative to the directory containing the project configuration file.
2233+
<<<
2234+
2235+
[[/SECTION]]
2236+
21962237
[[/SECTION]]
21972238

21982239
[[/SECTION]]

docs/strictdoc_04_release_notes.sdoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ This release contains the following enhancements:
5757
1\) The code block syntax highlighting has been enabled for all RST ``.. code::`` blocks. The code blocks that have been tested already include C, TOML, and Python.
5858

5959
Also, a Pygments lexer for the SDoc markup has been added to the StrictDoc source code. From now on, ``.. code:: strictdoc`` blocks are recognized and syntax-highlighted.
60+
61+
2\) The grammar aliases were added to allow importing a single grammar from multiple SDoc documents located in different directories. From now on, it is possible to register a grammar alias and its path in a project configuration file and use it as follows:
62+
63+
.. code-block:: strictdoc
64+
65+
[GRAMMAR]
66+
IMPORT_FROM_FILE: @my_grammar
67+
68+
Thanks to @gregshue for requesting this feature and explaining the need for it.
6069
<<<
6170

6271
[[/SECTION]]

strictdoc/backend/sdoc/processor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os.path
2+
import re
23
from typing import Any, Callable, Dict, List, Optional, Union, cast
34

45
from textx import TextXSyntaxError, get_model
@@ -89,6 +90,14 @@ def process_document_grammar(
8990
) -> None:
9091
assert self.parse_context.document_config is not None
9192

93+
if (import_from_file_ := document_grammar.import_from_file) is not None:
94+
if re.search(r"\.\.|[/\\]", import_from_file_):
95+
raise StrictDocException(
96+
"[GRAMMAR]: "
97+
"IMPORT_FROM_FILE must not contain any '..', '/', '\\' characters: "
98+
f"{import_from_file_}."
99+
)
100+
92101
preserve_source_location_data(document_grammar)
93102
# FIXME: It would be great to move forward and remove this.
94103
if not document_grammar.has_text_element():

strictdoc/cli/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ def _main_internal(parallelizer: Parallelizer, parser: SDocArgsParser) -> None:
8181
project_config = ProjectConfigLoader.load_from_path_or_get_default(
8282
path_to_config=server_config.get_path_to_config(),
8383
)
84-
project_config.validate_and_finalize()
8584
run_strictdoc_server(
8685
server_config=server_config, project_config=project_config
8786
)

strictdoc/core/file_system/document_finder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ def _build_document_tree(
164164
)
165165

166166
if isinstance(document, DocumentGrammar):
167-
map_grammars_by_filenames[doc_file.file_name] = document
167+
map_grammars_by_filenames[
168+
doc_file.rel_path.relative_path_posix
169+
] = document
168170
continue
169171

170172
input_doc_full_path: str = doc_file.full_path

strictdoc/core/project_config.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def __init__(
119119
source_root_path: Optional[str] = None,
120120
include_source_paths: Optional[List[str]] = None,
121121
exclude_source_paths: Optional[List[str]] = None,
122+
grammars: Optional[Dict[str, str]] = None,
122123
test_report_root_dict: Optional[Dict[str, str]] = None,
123124
source_nodes: Optional[List[SourceNodesEntry]] = None,
124125
html2pdf_strict: bool = False,
@@ -202,6 +203,9 @@ def __init__(
202203
self.exclude_source_paths: List[str] = (
203204
exclude_source_paths if exclude_source_paths is not None else []
204205
)
206+
207+
self.grammars: Dict[str, str] = grammars or {}
208+
205209
self.test_report_root_dict: Dict[str, str] = (
206210
test_report_root_dict if test_report_root_dict is not None else {}
207211
)
@@ -377,24 +381,45 @@ def integrate_export_config(
377381
self.reqif_enable_mid = export_config.reqif_enable_mid
378382

379383
def validate_and_finalize(self) -> None:
380-
if (input_paths_ := self.input_paths) and len(input_paths_) > 0:
381-
path_to_gitignore = os.path.join(self.input_paths[0], ".gitignore")
382-
if os.path.isfile(path_to_gitignore):
383-
patterns = ["/.git/"]
384-
385-
with open(path_to_gitignore, encoding="utf-8") as f:
386-
for line_ in f:
387-
line = line_.strip()
388-
if not line or line.startswith("#"):
389-
continue
390-
# Ignore !-negated gitignores for now or reimplement
391-
# using a dedicated gitignore Python library.
392-
if line.startswith("!"):
393-
continue
394-
patterns.append(line)
395-
396-
self.exclude_doc_paths.extend(patterns)
397-
self.exclude_source_paths.extend(patterns)
384+
project_path = self.get_project_root_path()
385+
386+
#
387+
# Read exclude paths from .gitignore. Add them to the user project's
388+
# both SDoc and source file search paths.
389+
#
390+
path_to_gitignore = os.path.join(project_path, ".gitignore")
391+
if os.path.isfile(path_to_gitignore):
392+
patterns = ["/.git/"]
393+
394+
with open(path_to_gitignore, encoding="utf-8") as f:
395+
for line_ in f:
396+
line = line_.strip()
397+
if not line or line.startswith("#"):
398+
continue
399+
# Ignore !-negated gitignores for now or reimplement
400+
# using a dedicated gitignore Python library.
401+
if line.startswith("!"):
402+
continue
403+
patterns.append(line)
404+
405+
self.exclude_doc_paths.extend(patterns)
406+
self.exclude_source_paths.extend(patterns)
407+
408+
#
409+
# Validate that the provided grammar shortcuts all point to existing
410+
# grammar files.
411+
#
412+
for grammar_alias_, grammar_path_ in self.grammars.items():
413+
assert grammar_alias_.startswith("@"), (
414+
"Grammar alias must start with an '@' character."
415+
)
416+
assert "." not in grammar_alias_, (
417+
"Grammar alias must not contain any . characters."
418+
)
419+
assert os.path.isfile(os.path.join(project_path, grammar_path_)), (
420+
"Grammar path must point to an existing path relative to the "
421+
f"project config file: {grammar_path_}."
422+
)
398423

399424
def is_feature_activated(self, feature: ProjectFeature) -> bool:
400425
return feature in self.project_features
@@ -459,6 +484,11 @@ def is_activated_source_file_language_parsers(self) -> bool:
459484
ProjectFeature.SOURCE_FILE_LANGUAGE_PARSERS in self.project_features
460485
)
461486

487+
def get_project_root_path(self) -> str:
488+
if self.input_paths is not None and len(self.input_paths) > 0:
489+
return self.input_paths[0]
490+
raise NotImplementedError
491+
462492
def get_strictdoc_root_path(self) -> str:
463493
return self.environment.path_to_strictdoc
464494

strictdoc/core/traceability_index_builder.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datetime
66
import glob
77
import os
8+
import posixpath
89
import sys
910
from typing import Any, Dict, Iterator, List, Optional, Set, Union
1011

@@ -378,10 +379,16 @@ def create_from_document_tree(
378379
# First, resolve all grammars that are imported from grammar files.
379380
#
380381
if document.grammar.import_from_file is not None:
381-
document_grammar: Optional[DocumentGrammar] = (
382-
document_tree.get_grammar_by_filename(
383-
document.grammar.import_from_file
382+
grammar_path = document.grammar.import_from_file
383+
if grammar_path.startswith("@"):
384+
grammar_path = project_config.grammars[grammar_path]
385+
else:
386+
grammar_path = posixpath.join(
387+
document.meta.input_doc_dir_rel_path.relative_path_posix,
388+
grammar_path,
384389
)
390+
document_grammar: Optional[DocumentGrammar] = (
391+
document_tree.get_grammar_by_filename(grammar_path)
385392
)
386393
if document_grammar is None:
387394
raise StrictDocException(

strictdoc/server/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def run_strictdoc_server(
4545
)
4646

4747
project_config.integrate_server_config(server_config)
48+
project_config.validate_and_finalize()
4849

4950
reload_config = UvicornReloadConfig.create(
5051
project_config, server_config

0 commit comments

Comments
 (0)