doclang 0.4.0__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.
- doclang/README.md +106 -0
- doclang/__init__.py +5 -0
- doclang/_schemas.py +23 -0
- doclang/cli.py +172 -0
- doclang/doclang.sch +352 -0
- doclang/doclang.xsd +692 -0
- doclang/schematron_validation.py +205 -0
- doclang/utils.py +66 -0
- doclang/validation.py +104 -0
- doclang/version.py +93 -0
- doclang/xsd_validation.py +52 -0
- doclang-0.4.0.dist-info/METADATA +24 -0
- doclang-0.4.0.dist-info/RECORD +17 -0
- doclang-0.4.0.dist-info/WHEEL +5 -0
- doclang-0.4.0.dist-info/entry_points.txt +2 -0
- doclang-0.4.0.dist-info/licenses/LICENSE +201 -0
- doclang-0.4.0.dist-info/top_level.txt +1 -0
doclang/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# DocLang Validation
|
|
2
|
+
|
|
3
|
+
Validate DocLang XML documents against XSD schema and Schematron rules.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install doclang
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic CLI Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
doclang validate my_document.dclg.xml
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### More CLI Usage Scenarios
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
## Inject DocLang namespace if document doesn't declare it:
|
|
23
|
+
doclang validate my_document.dclg.xml --allow-empty-namespace
|
|
24
|
+
|
|
25
|
+
# XSD validation only
|
|
26
|
+
doclang validate my_document.dclg.xml --xsd-only
|
|
27
|
+
|
|
28
|
+
# Schematron validation only
|
|
29
|
+
doclang validate my_document.dclg.xml --schematron-only
|
|
30
|
+
|
|
31
|
+
# JSON output
|
|
32
|
+
doclang validate my_document.dclg.xml --format json
|
|
33
|
+
|
|
34
|
+
# Quiet mode (exit code only)
|
|
35
|
+
doclang validate my_document.dclg.xml --quiet
|
|
36
|
+
|
|
37
|
+
# Show help
|
|
38
|
+
doclang --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Python API
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from doclang import validate, ValidationError
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
validate("my_document.dclg.xml")
|
|
48
|
+
print("Validation OK (no exception)")
|
|
49
|
+
except ValidationError as exc:
|
|
50
|
+
print(exc) # human-readable summary
|
|
51
|
+
print(f"{exc.xsd_errors=}")
|
|
52
|
+
print(f"{exc.schematron_errors=}")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Validation Rules
|
|
56
|
+
|
|
57
|
+
### XSD Validation (doclang.xsd)
|
|
58
|
+
|
|
59
|
+
Standard XML Schema Definition for structural validation:
|
|
60
|
+
|
|
61
|
+
- Document structure and element hierarchy
|
|
62
|
+
- Data types and attributes
|
|
63
|
+
- Element ordering
|
|
64
|
+
|
|
65
|
+
### Schematron Rules (doclang.sch)
|
|
66
|
+
|
|
67
|
+
Additional business rules that XSD cannot express, using XSLT 3.0 and XPath 3.1:
|
|
68
|
+
|
|
69
|
+
```xml
|
|
70
|
+
<sch:pattern id="my-rule">
|
|
71
|
+
<sch:rule context="dl:element">
|
|
72
|
+
<sch:assert test="condition">Error message</sch:assert>
|
|
73
|
+
</sch:rule>
|
|
74
|
+
</sch:pattern>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The validation uses XSLT 3.0 for modern XPath features.
|
|
78
|
+
|
|
79
|
+
## XSD Validation with VS Code
|
|
80
|
+
|
|
81
|
+
In VS Code you can use [Red Hat's XML extension](https://open-vsx.org/vscode/item?itemName=redhat.vscode-xml) and enable IDE-native XSD validation by adding the following to your `settings.json` (ℹ️ replacing the actual XSD path):
|
|
82
|
+
|
|
83
|
+
```xml
|
|
84
|
+
"xml.fileAssociations": [
|
|
85
|
+
{
|
|
86
|
+
"pattern": "**/*.dclg.xml",
|
|
87
|
+
"systemId": "file:///absolute/path/to/doclang.xsd",
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For this to work, the DocLang XML document must include the relevant namespace:
|
|
93
|
+
|
|
94
|
+
```xml
|
|
95
|
+
<doclang xmlns="https://www.doclang.ai/ns/v0">
|
|
96
|
+
<!-- ... -->
|
|
97
|
+
</doclang>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Note that this approach does not cover Schematron validation rules.
|
|
101
|
+
|
|
102
|
+
## References
|
|
103
|
+
|
|
104
|
+
- [XSD 1.0 Specification](https://www.w3.org/TR/xmlschema-1/)
|
|
105
|
+
- [ISO Schematron](http://schematron.com/)
|
|
106
|
+
- [XPath 3.1 Specification](https://www.w3.org/TR/xpath-31/)
|
doclang/__init__.py
ADDED
doclang/_schemas.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Bundled DocLang schema paths (internal)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_SCHEMA_DIR = Path(__file__).resolve().parent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _bundled_xsd_path() -> Path:
|
|
9
|
+
path = _SCHEMA_DIR / "doclang.xsd"
|
|
10
|
+
if not path.exists():
|
|
11
|
+
raise FileNotFoundError(f"Bundled XSD schema not found: {path}")
|
|
12
|
+
return path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _bundled_sch_path() -> Path:
|
|
16
|
+
path = _SCHEMA_DIR / "doclang.sch"
|
|
17
|
+
if not path.exists():
|
|
18
|
+
raise FileNotFoundError(f"Bundled Schematron schema not found: {path}")
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bundled_schema_paths() -> tuple[Path, Path]:
|
|
23
|
+
return _bundled_xsd_path(), _bundled_sch_path()
|
doclang/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DocLang CLI - Command-line interface for XML validation.
|
|
3
|
+
|
|
4
|
+
Provides a user-friendly CLI using Typer for validating DocLang XML documents
|
|
5
|
+
against XSD schemas and Schematron rules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from doclang._schemas import _bundled_schema_paths
|
|
16
|
+
from doclang.utils import _VERSION
|
|
17
|
+
from doclang.validation import ValidationError
|
|
18
|
+
from doclang.validation import validate as validate_document
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="doclang",
|
|
22
|
+
help="DocLang XML validation tool with XSD and Schematron support",
|
|
23
|
+
add_completion=False,
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OutputFormat(str, Enum):
|
|
29
|
+
"""Output format options."""
|
|
30
|
+
|
|
31
|
+
text = "text"
|
|
32
|
+
json = "json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command(
|
|
36
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
def validate(
|
|
40
|
+
xml_file: Path = typer.Argument(..., help="XML file to validate", exists=True),
|
|
41
|
+
xsd_only: bool = typer.Option(False, "--xsd-only", help="Validate XSD only"),
|
|
42
|
+
schematron_only: bool = typer.Option(False, "--schematron-only", help="Validate Schematron only"),
|
|
43
|
+
allow_empty_namespace: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--allow-empty-namespace",
|
|
46
|
+
"-n",
|
|
47
|
+
help="Allow documents without namespace (auto-inject DocLang namespace)",
|
|
48
|
+
),
|
|
49
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Quiet mode (exit code only)"),
|
|
50
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
51
|
+
format: OutputFormat = typer.Option(OutputFormat.text, "--format", "-f", help="Output format"),
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Validate XML document against bundled XSD schema and Schematron rules.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
|
|
58
|
+
doclang validate document.xml
|
|
59
|
+
doclang validate document.xml --xsd-only
|
|
60
|
+
doclang validate document.xml --format json
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
bundled_xsd, bundled_sch = _bundled_schema_paths()
|
|
64
|
+
except FileNotFoundError as exc:
|
|
65
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
if not quiet and format == OutputFormat.text:
|
|
69
|
+
typer.echo(f"Validating: {xml_file}")
|
|
70
|
+
typer.echo("-" * 60)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
validate_document(
|
|
74
|
+
xml_file,
|
|
75
|
+
allow_empty_namespace=allow_empty_namespace,
|
|
76
|
+
xsd_only=xsd_only,
|
|
77
|
+
schematron_only=schematron_only,
|
|
78
|
+
)
|
|
79
|
+
except ValidationError as exc:
|
|
80
|
+
results: dict[str, Any] = {
|
|
81
|
+
"file": exc.file,
|
|
82
|
+
"xsd": {"valid": exc.xsd_valid, "errors": exc.xsd_errors},
|
|
83
|
+
"schematron": {"valid": exc.schematron_valid, "errors": exc.schematron_errors},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if not quiet and format == OutputFormat.text:
|
|
87
|
+
if not schematron_only:
|
|
88
|
+
if verbose:
|
|
89
|
+
typer.echo("XSD Validation")
|
|
90
|
+
typer.echo(f"Schema: {bundled_xsd}")
|
|
91
|
+
if exc.xsd_valid:
|
|
92
|
+
typer.echo("XSD validation passed")
|
|
93
|
+
else:
|
|
94
|
+
typer.echo("XSD validation failed")
|
|
95
|
+
for error in exc.xsd_errors:
|
|
96
|
+
if "line" in error:
|
|
97
|
+
typer.echo(f" Line {error['line']}: {error['message']}")
|
|
98
|
+
else:
|
|
99
|
+
typer.echo(f" {error.get('error', 'Unknown error')}")
|
|
100
|
+
|
|
101
|
+
if not xsd_only and (exc.xsd_valid or schematron_only):
|
|
102
|
+
if verbose:
|
|
103
|
+
typer.echo("Schematron Validation")
|
|
104
|
+
typer.echo(f"Schema: {bundled_sch}")
|
|
105
|
+
if exc.schematron_valid:
|
|
106
|
+
typer.echo("Schematron validation passed")
|
|
107
|
+
else:
|
|
108
|
+
typer.echo("Schematron validation failed")
|
|
109
|
+
for error in exc.schematron_errors:
|
|
110
|
+
typer.echo(f" {error['location']}")
|
|
111
|
+
typer.echo(f" {error['message']}")
|
|
112
|
+
|
|
113
|
+
if format == OutputFormat.json:
|
|
114
|
+
typer.echo(json.dumps(results, indent=2))
|
|
115
|
+
elif not quiet:
|
|
116
|
+
typer.echo("-" * 60)
|
|
117
|
+
typer.echo("VALIDATION FAILED")
|
|
118
|
+
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
if not quiet and format == OutputFormat.text:
|
|
122
|
+
if not schematron_only:
|
|
123
|
+
if verbose:
|
|
124
|
+
typer.echo("XSD Validation")
|
|
125
|
+
typer.echo(f"Schema: {bundled_xsd}")
|
|
126
|
+
typer.echo("XSD validation passed")
|
|
127
|
+
|
|
128
|
+
if not xsd_only:
|
|
129
|
+
if verbose:
|
|
130
|
+
typer.echo("Schematron Validation")
|
|
131
|
+
typer.echo(f"Schema: {bundled_sch}")
|
|
132
|
+
typer.echo("Schematron validation passed")
|
|
133
|
+
|
|
134
|
+
if format == OutputFormat.json:
|
|
135
|
+
typer.echo(
|
|
136
|
+
json.dumps(
|
|
137
|
+
{
|
|
138
|
+
"file": str(xml_file),
|
|
139
|
+
"xsd": {"valid": True, "errors": []},
|
|
140
|
+
"schematron": {"valid": True, "errors": []},
|
|
141
|
+
},
|
|
142
|
+
indent=2,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
elif not quiet:
|
|
146
|
+
typer.echo("-" * 60)
|
|
147
|
+
typer.echo("VALIDATION SUCCESSFUL")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _version_callback(value: bool):
|
|
151
|
+
"""Show version and exit."""
|
|
152
|
+
if value:
|
|
153
|
+
typer.echo(f"doclang version {_VERSION}")
|
|
154
|
+
raise typer.Exit()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.callback()
|
|
158
|
+
def main(
|
|
159
|
+
version: bool = typer.Option(
|
|
160
|
+
None,
|
|
161
|
+
"--version",
|
|
162
|
+
"-v",
|
|
163
|
+
callback=_version_callback,
|
|
164
|
+
is_eager=True,
|
|
165
|
+
help="Show version and exit",
|
|
166
|
+
),
|
|
167
|
+
):
|
|
168
|
+
"""DocLang XML validation tool."""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
app()
|
doclang/doclang.sch
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"
|
|
3
|
+
xmlns:dl="https://www.doclang.ai/ns/v0"
|
|
4
|
+
queryBinding="xslt3">
|
|
5
|
+
|
|
6
|
+
<sch:title>Doclang Schematron Validation Rules (XSLT 3.0)</sch:title>
|
|
7
|
+
|
|
8
|
+
<sch:ns prefix="dl" uri="https://www.doclang.ai/ns/v0"/>
|
|
9
|
+
|
|
10
|
+
<!-- ============================================ -->
|
|
11
|
+
<!-- NOTE: Element head order is enforced by XSD element_head group -->
|
|
12
|
+
<!-- Schematron only validates structural tokens (ldiv for lists, cell tokens for tables) -->
|
|
13
|
+
<!-- ============================================ -->
|
|
14
|
+
|
|
15
|
+
<!-- ============================================ -->
|
|
16
|
+
<!-- LIST: Must start with ldiv (after optional element head) -->
|
|
17
|
+
<!-- ============================================ -->
|
|
18
|
+
|
|
19
|
+
<sch:pattern id="list-structure">
|
|
20
|
+
<sch:rule context="dl:list[*]">
|
|
21
|
+
<sch:let name="first-non-header" value="*[not(self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom)][1]"/>
|
|
22
|
+
|
|
23
|
+
<sch:assert test="not($first-non-header) or $first-non-header[self::dl:ldiv]">
|
|
24
|
+
List must have ldiv as first element after optional element head (property elements: label, thread, xref, href, location, caption, custom).
|
|
25
|
+
Found: <sch:value-of select="if ($first-non-header) then name($first-non-header) else 'nothing'"/>
|
|
26
|
+
</sch:assert>
|
|
27
|
+
</sch:rule>
|
|
28
|
+
</sch:pattern>
|
|
29
|
+
|
|
30
|
+
<!-- ============================================ -->
|
|
31
|
+
<!-- TABLE: Must start with cell token (after optional element head) -->
|
|
32
|
+
<!-- ============================================ -->
|
|
33
|
+
|
|
34
|
+
<sch:pattern id="table-structure">
|
|
35
|
+
<sch:rule context="dl:table[*] | dl:index[*]">
|
|
36
|
+
<sch:let name="first-non-header" value="*[not(self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom)][1]"/>
|
|
37
|
+
|
|
38
|
+
<sch:assert test="not($first-non-header) or
|
|
39
|
+
$first-non-header[self::dl:fcel or self::dl:ecel or self::dl:ched or
|
|
40
|
+
self::dl:rhed or self::dl:corn or self::dl:srow or
|
|
41
|
+
self::dl:lcel or self::dl:ucel or self::dl:xcel]">
|
|
42
|
+
Table and index must have cell-starting token as first element after optional element head (property elements: label, thread, xref, href, location, caption, custom).
|
|
43
|
+
Found: <sch:value-of select="if ($first-non-header) then name($first-non-header) else 'nothing'"/>
|
|
44
|
+
</sch:assert>
|
|
45
|
+
</sch:rule>
|
|
46
|
+
</sch:pattern>
|
|
47
|
+
|
|
48
|
+
<!-- ============================================ -->
|
|
49
|
+
<!-- TABLE: Rectangular grid validation -->
|
|
50
|
+
<!-- Ensures all rows have the same number of columns -->
|
|
51
|
+
<!-- ============================================ -->
|
|
52
|
+
|
|
53
|
+
<sch:pattern id="table-rectangular-grid">
|
|
54
|
+
<sch:rule context="dl:table[dl:nl] | dl:index[dl:nl]">
|
|
55
|
+
<!-- Define cell-starting tokens (tokens that begin a new cell) -->
|
|
56
|
+
<sch:let name="cell-tokens" value="dl:fcel | dl:ecel | dl:ched | dl:rhed | dl:corn | dl:srow | dl:lcel | dl:ucel | dl:xcel"/>
|
|
57
|
+
|
|
58
|
+
<!-- Count cells in first row (before first nl) -->
|
|
59
|
+
<sch:let name="first-row-cells" value="count($cell-tokens[following-sibling::dl:nl[1] is current()/dl:nl[1]])"/>
|
|
60
|
+
|
|
61
|
+
<!-- Check that all subsequent rows have the same number of cells -->
|
|
62
|
+
<sch:assert test="every $nl in dl:nl[position() > 1] satisfies
|
|
63
|
+
count($cell-tokens[preceding-sibling::dl:nl[1] is $nl/preceding-sibling::dl:nl[1] and
|
|
64
|
+
following-sibling::dl:nl[1] is $nl]) = $first-row-cells">
|
|
65
|
+
Table and index must follow the rectangular rule: all rows must have the same number of cells.
|
|
66
|
+
First row has <sch:value-of select="$first-row-cells"/> cells, but at least one other row has a different count.
|
|
67
|
+
Each row should have the same count of cell-starting tokens (fcel, ecel, ched, rhed, corn, srow, lcel, ucel, xcel) before each nl element.
|
|
68
|
+
</sch:assert>
|
|
69
|
+
</sch:rule>
|
|
70
|
+
</sch:pattern>
|
|
71
|
+
|
|
72
|
+
<!-- ============================================ -->
|
|
73
|
+
<!-- ELEMENT HEAD: Text must not precede property elements -->
|
|
74
|
+
<!-- Property elements: label, thread, xref, href, location, caption, custom (per XSD element_head group) -->
|
|
75
|
+
<!-- This rule applies to regular semantic elements AND virtual <text> in lists/tables -->
|
|
76
|
+
<!-- ============================================ -->
|
|
77
|
+
|
|
78
|
+
<sch:pattern id="element-head-placement">
|
|
79
|
+
<sch:rule context="dl:text | dl:heading | dl:code | dl:formula | dl:caption |
|
|
80
|
+
dl:page_header | dl:page_footer | dl:footnote | dl:picture | dl:marker |
|
|
81
|
+
dl:field_region | dl:field_heading | dl:field_item | dl:key | dl:value |
|
|
82
|
+
dl:list | dl:table | dl:index | dl:group">
|
|
83
|
+
<sch:let name="header-elements" value="dl:label | dl:thread | dl:xref | dl:href | dl:location | dl:caption | dl:custom"/>
|
|
84
|
+
|
|
85
|
+
<sch:let name="text-before-header" value="text()[following-sibling::*[self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom]]"/>
|
|
86
|
+
|
|
87
|
+
<sch:assert test="every $t in $text-before-header satisfies normalize-space($t) = ''">
|
|
88
|
+
Property elements in the element head (label, thread, xref, href, location, caption, custom) must appear before any non-whitespace text content.
|
|
89
|
+
Found non-whitespace text before element head: '<sch:value-of select="normalize-space(string-join($text-before-header, ''))"/>'
|
|
90
|
+
</sch:assert>
|
|
91
|
+
</sch:rule>
|
|
92
|
+
</sch:pattern>
|
|
93
|
+
|
|
94
|
+
<!-- ============================================ -->
|
|
95
|
+
<!-- ELEMENT HEAD: xref and href are mutually exclusive -->
|
|
96
|
+
<!-- ============================================ -->
|
|
97
|
+
|
|
98
|
+
<sch:pattern id="xref-href-mutual-exclusivity">
|
|
99
|
+
<sch:rule context="*[dl:xref and dl:href]">
|
|
100
|
+
<sch:assert test="false()">
|
|
101
|
+
Element head must not contain both xref and href elements; they are mutually exclusive.
|
|
102
|
+
</sch:assert>
|
|
103
|
+
</sch:rule>
|
|
104
|
+
</sch:pattern>
|
|
105
|
+
|
|
106
|
+
<!-- ============================================ -->
|
|
107
|
+
<!-- XREF: referenced thread_id must be defined by at least one thread element -->
|
|
108
|
+
<!-- ============================================ -->
|
|
109
|
+
|
|
110
|
+
<sch:pattern id="xref-thread-defined">
|
|
111
|
+
<sch:rule context="dl:xref">
|
|
112
|
+
<sch:assert test="exists(//dl:thread[@thread_id = current()/@thread_id])">
|
|
113
|
+
Element xref references thread_id="<sch:value-of select="@thread_id"/>" but no thread element defines that id.
|
|
114
|
+
</sch:assert>
|
|
115
|
+
</sch:rule>
|
|
116
|
+
</sch:pattern>
|
|
117
|
+
|
|
118
|
+
<!-- ============================================ -->
|
|
119
|
+
<!-- LOCATION: value must be within [0, axis_limit) -->
|
|
120
|
+
<!-- axis_limit precedence: location@resolution, head/default_resolution axis, fallback 512 -->
|
|
121
|
+
<!-- ============================================ -->
|
|
122
|
+
|
|
123
|
+
<sch:pattern id="location-value-range">
|
|
124
|
+
<sch:rule context="dl:location">
|
|
125
|
+
<sch:let name="location-index" value="count(preceding-sibling::dl:location) + 1"/>
|
|
126
|
+
<sch:let name="is-x-axis" value="$location-index mod 2 = 1"/>
|
|
127
|
+
<sch:let name="doc-default-width" value="if (/dl:doclang/dl:head[1]/dl:default_resolution[1]/@width)
|
|
128
|
+
then number(/dl:doclang/dl:head[1]/dl:default_resolution[1]/@width)
|
|
129
|
+
else 512"/>
|
|
130
|
+
<sch:let name="doc-default-height" value="if (/dl:doclang/dl:head[1]/dl:default_resolution[1]/@height)
|
|
131
|
+
then number(/dl:doclang/dl:head[1]/dl:default_resolution[1]/@height)
|
|
132
|
+
else 512"/>
|
|
133
|
+
<sch:let name="axis-limit" value="if (@resolution)
|
|
134
|
+
then number(@resolution)
|
|
135
|
+
else if ($is-x-axis)
|
|
136
|
+
then $doc-default-width
|
|
137
|
+
else $doc-default-height"/>
|
|
138
|
+
|
|
139
|
+
<sch:assert test="number(@value) ge 0 and number(@value) lt $axis-limit">
|
|
140
|
+
Location value must satisfy 0 <= value < axis_limit.
|
|
141
|
+
Found value=<sch:value-of select="@value"/>, axis_limit=<sch:value-of select="$axis-limit"/>,
|
|
142
|
+
axis=<sch:value-of select="if ($is-x-axis) then 'x' else 'y'"/>,
|
|
143
|
+
location-index=<sch:value-of select="$location-index"/>.
|
|
144
|
+
</sch:assert>
|
|
145
|
+
</sch:rule>
|
|
146
|
+
</sch:pattern>
|
|
147
|
+
|
|
148
|
+
<!-- ============================================ -->
|
|
149
|
+
<!-- LOCATION BLOCK: enforce x0<=x1 and y0<=y1 -->
|
|
150
|
+
<!-- ============================================ -->
|
|
151
|
+
|
|
152
|
+
<sch:pattern id="location-block-order">
|
|
153
|
+
<sch:rule context="*[dl:location]">
|
|
154
|
+
<sch:let name="x0" value="number(dl:location[1]/@value)"/>
|
|
155
|
+
<sch:let name="y0" value="number(dl:location[2]/@value)"/>
|
|
156
|
+
<sch:let name="x1" value="number(dl:location[3]/@value)"/>
|
|
157
|
+
<sch:let name="y1" value="number(dl:location[4]/@value)"/>
|
|
158
|
+
|
|
159
|
+
<!-- Effective resolution for each coordinate -->
|
|
160
|
+
<sch:let name="doc-default-width" value="if (/dl:doclang/dl:head[1]/dl:default_resolution[1]/@width)
|
|
161
|
+
then number(/dl:doclang/dl:head[1]/dl:default_resolution[1]/@width)
|
|
162
|
+
else 512"/>
|
|
163
|
+
<sch:let name="doc-default-height" value="if (/dl:doclang/dl:head[1]/dl:default_resolution[1]/@height)
|
|
164
|
+
then number(/dl:doclang/dl:head[1]/dl:default_resolution[1]/@height)
|
|
165
|
+
else 512"/>
|
|
166
|
+
|
|
167
|
+
<sch:let name="x0-res" value="if (dl:location[1]/@resolution)
|
|
168
|
+
then number(dl:location[1]/@resolution)
|
|
169
|
+
else $doc-default-width"/>
|
|
170
|
+
<sch:let name="y0-res" value="if (dl:location[2]/@resolution)
|
|
171
|
+
then number(dl:location[2]/@resolution)
|
|
172
|
+
else $doc-default-height"/>
|
|
173
|
+
<sch:let name="x1-res" value="if (dl:location[3]/@resolution)
|
|
174
|
+
then number(dl:location[3]/@resolution)
|
|
175
|
+
else $doc-default-width"/>
|
|
176
|
+
<sch:let name="y1-res" value="if (dl:location[4]/@resolution)
|
|
177
|
+
then number(dl:location[4]/@resolution)
|
|
178
|
+
else $doc-default-height"/>
|
|
179
|
+
|
|
180
|
+
<sch:let name="x0-norm" value="$x0 div $x0-res"/>
|
|
181
|
+
<sch:let name="y0-norm" value="$y0 div $y0-res"/>
|
|
182
|
+
<sch:let name="x1-norm" value="$x1 div $x1-res"/>
|
|
183
|
+
<sch:let name="y1-norm" value="$y1 div $y1-res"/>
|
|
184
|
+
|
|
185
|
+
<sch:assert test="$x0-norm le $x1-norm and $y0-norm le $y1-norm">
|
|
186
|
+
Location block must satisfy x0_norm <= x1_norm and y0_norm <= y1_norm,
|
|
187
|
+
where *_norm is each coordinate normalized by its effective resolution.
|
|
188
|
+
Found:
|
|
189
|
+
x0=<sch:value-of select="$x0"/>, x0_res=<sch:value-of select="$x0-res"/>, x0_norm=<sch:value-of select="$x0-norm"/>,
|
|
190
|
+
x1=<sch:value-of select="$x1"/>, x1_res=<sch:value-of select="$x1-res"/>, x1_norm=<sch:value-of select="$x1-norm"/>,
|
|
191
|
+
y0=<sch:value-of select="$y0"/>, y0_res=<sch:value-of select="$y0-res"/>, y0_norm=<sch:value-of select="$y0-norm"/>,
|
|
192
|
+
y1=<sch:value-of select="$y1"/>, y1_res=<sch:value-of select="$y1-res"/>, y1_norm=<sch:value-of select="$y1-norm"/>.
|
|
193
|
+
</sch:assert>
|
|
194
|
+
</sch:rule>
|
|
195
|
+
</sch:pattern>
|
|
196
|
+
|
|
197
|
+
<!-- ============================================ -->
|
|
198
|
+
<!-- THREAD: same thread_id must not span different host element types -->
|
|
199
|
+
<!-- Host type is the parent semantic element (list, list-item, table, table-cell, text, picture, etc.) -->
|
|
200
|
+
<!-- ============================================ -->
|
|
201
|
+
|
|
202
|
+
<sch:pattern id="thread-host-type-consistency">
|
|
203
|
+
<sch:rule context="dl:doclang">
|
|
204
|
+
<sch:let name="threads" value="//dl:thread"/>
|
|
205
|
+
<sch:let name="thread-ids" value="distinct-values($threads/@thread_id)"/>
|
|
206
|
+
<sch:let name="cell-token-names" value="('fcel','ecel','ched','rhed','corn','srow','lcel','ucel','xcel')"/>
|
|
207
|
+
<sch:assert test="every $tid in $thread-ids satisfies
|
|
208
|
+
count(distinct-values(
|
|
209
|
+
for $t in $threads[@thread_id = $tid]
|
|
210
|
+
return
|
|
211
|
+
if ($t/parent::dl:list) then
|
|
212
|
+
(if ($t/preceding-sibling::dl:ldiv) then 'list-item' else 'list')
|
|
213
|
+
else if ($t/parent::dl:table or $t/parent::dl:index) then
|
|
214
|
+
(if ($t/preceding-sibling::*[local-name() = $cell-token-names]) then 'table-cell' else local-name($t/parent::*))
|
|
215
|
+
else local-name($t/parent::*)
|
|
216
|
+
)) = 1">
|
|
217
|
+
All thread elements with the same thread_id must use the same host element type
|
|
218
|
+
(e.g. all text, not text and picture). Check thread_id values for mixed types.
|
|
219
|
+
</sch:assert>
|
|
220
|
+
</sch:rule>
|
|
221
|
+
</sch:pattern>
|
|
222
|
+
|
|
223
|
+
<!-- ============================================ -->
|
|
224
|
+
<!-- VIRTUAL TEXT IN LISTS: Element head must precede content -->
|
|
225
|
+
<!-- A list item (content between <ldiv> siblings) acts as a virtual <text> -->
|
|
226
|
+
<!-- and must follow the same element head rules -->
|
|
227
|
+
<!-- ============================================ -->
|
|
228
|
+
|
|
229
|
+
<sch:pattern id="list-virtual-text-element-head">
|
|
230
|
+
<sch:rule context="dl:list/dl:ldiv">
|
|
231
|
+
<sch:let name="next-ldiv" value="following-sibling::dl:ldiv[1]"/>
|
|
232
|
+
|
|
233
|
+
<sch:let name="item-content" value="if ($next-ldiv)
|
|
234
|
+
then following-sibling::node()[following-sibling::dl:ldiv[1] is $next-ldiv]
|
|
235
|
+
else following-sibling::node()"/>
|
|
236
|
+
|
|
237
|
+
<sch:let name="header-elements" value="$item-content[self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom]"/>
|
|
238
|
+
|
|
239
|
+
<sch:let name="first-header-index" value="if ($header-elements)
|
|
240
|
+
then index-of($item-content, $header-elements[1])[1]
|
|
241
|
+
else 0"/>
|
|
242
|
+
|
|
243
|
+
<sch:let name="text-before-header" value="if ($first-header-index > 0)
|
|
244
|
+
then for $i in 1 to ($first-header-index - 1)
|
|
245
|
+
return $item-content[$i][self::text()][normalize-space(.) != '']
|
|
246
|
+
else ()"/>
|
|
247
|
+
|
|
248
|
+
<sch:assert test="empty($text-before-header)">
|
|
249
|
+
In list items (virtual text), property elements in the element head (label, thread, xref, href, location, caption, custom) must appear before any non-whitespace text content.
|
|
250
|
+
Found non-whitespace text before element head: '<sch:value-of select="normalize-space(string-join($text-before-header, ''))"/>'
|
|
251
|
+
</sch:assert>
|
|
252
|
+
</sch:rule>
|
|
253
|
+
</sch:pattern>
|
|
254
|
+
|
|
255
|
+
<!-- ============================================ -->
|
|
256
|
+
<!-- VIRTUAL TEXT IN TABLES: Element head must precede content -->
|
|
257
|
+
<!-- A table cell (content after cell-starting tokens) acts as a virtual <text> -->
|
|
258
|
+
<!-- and must follow the same element head rules -->
|
|
259
|
+
<!-- ============================================ -->
|
|
260
|
+
|
|
261
|
+
<sch:pattern id="table-virtual-text-element-head">
|
|
262
|
+
<sch:rule context="dl:table/dl:fcel | dl:table/dl:ecel | dl:table/dl:ched |
|
|
263
|
+
dl:table/dl:rhed | dl:table/dl:corn | dl:table/dl:srow |
|
|
264
|
+
dl:table/dl:lcel | dl:table/dl:ucel | dl:table/dl:xcel |
|
|
265
|
+
dl:index/dl:fcel | dl:index/dl:ecel | dl:index/dl:ched |
|
|
266
|
+
dl:index/dl:rhed | dl:index/dl:corn | dl:index/dl:srow |
|
|
267
|
+
dl:index/dl:lcel | dl:index/dl:ucel | dl:index/dl:xcel">
|
|
268
|
+
<sch:let name="next-token" value="following-sibling::*[self::dl:fcel or self::dl:ecel or self::dl:ched or
|
|
269
|
+
self::dl:rhed or self::dl:corn or self::dl:srow or
|
|
270
|
+
self::dl:lcel or self::dl:ucel or self::dl:xcel or
|
|
271
|
+
self::dl:nl][1]"/>
|
|
272
|
+
|
|
273
|
+
<sch:let name="cell-content" value="if ($next-token)
|
|
274
|
+
then following-sibling::node()[following-sibling::*[. is $next-token]]
|
|
275
|
+
else following-sibling::node()[not(following-sibling::dl:nl)]"/>
|
|
276
|
+
|
|
277
|
+
<sch:let name="header-elements" value="$cell-content[self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom]"/>
|
|
278
|
+
|
|
279
|
+
<sch:let name="first-header-index" value="if ($header-elements)
|
|
280
|
+
then index-of($cell-content, $header-elements[1])[1]
|
|
281
|
+
else 0"/>
|
|
282
|
+
|
|
283
|
+
<sch:let name="text-before-header" value="if ($first-header-index > 0)
|
|
284
|
+
then for $i in 1 to ($first-header-index - 1)
|
|
285
|
+
return $cell-content[$i][self::text()][normalize-space(.) != '']
|
|
286
|
+
else ()"/>
|
|
287
|
+
|
|
288
|
+
<sch:assert test="empty($text-before-header)">
|
|
289
|
+
In table and index cells (virtual text), property elements in the element head (label, thread, xref, href, location, caption, custom) must appear before any non-whitespace text content.
|
|
290
|
+
Found non-whitespace text before element head: '<sch:value-of select="normalize-space(string-join($text-before-header, ''))"/>'
|
|
291
|
+
</sch:assert>
|
|
292
|
+
</sch:rule>
|
|
293
|
+
</sch:pattern>
|
|
294
|
+
|
|
295
|
+
<!-- ============================================ -->
|
|
296
|
+
<!-- FIELD STRUCTURE: placement and key cardinality -->
|
|
297
|
+
<!-- Enforces Appendix A field ancestry constraints -->
|
|
298
|
+
<!-- ============================================ -->
|
|
299
|
+
<sch:pattern id="field-structure-placement">
|
|
300
|
+
<sch:rule context="dl:field_heading">
|
|
301
|
+
<sch:assert test="exists(ancestor::dl:field_region)">
|
|
302
|
+
field_heading and field_item must be descendants of field_region.
|
|
303
|
+
</sch:assert>
|
|
304
|
+
</sch:rule>
|
|
305
|
+
|
|
306
|
+
<sch:rule context="dl:field_item">
|
|
307
|
+
<sch:assert test="exists(ancestor::dl:field_region)">
|
|
308
|
+
field_heading and field_item must be descendants of field_region.
|
|
309
|
+
</sch:assert>
|
|
310
|
+
</sch:rule>
|
|
311
|
+
|
|
312
|
+
<sch:rule context="dl:key">
|
|
313
|
+
<sch:assert test="exists(ancestor::dl:field_item)">
|
|
314
|
+
key and value must be descendants of field_item.
|
|
315
|
+
</sch:assert>
|
|
316
|
+
</sch:rule>
|
|
317
|
+
|
|
318
|
+
<sch:rule context="dl:value">
|
|
319
|
+
<sch:assert test="exists(ancestor::dl:field_item)">
|
|
320
|
+
key and value must be descendants of field_item.
|
|
321
|
+
</sch:assert>
|
|
322
|
+
</sch:rule>
|
|
323
|
+
|
|
324
|
+
<sch:rule context="dl:field_item">
|
|
325
|
+
<sch:let name="own-key-count" value="count(.//dl:key[count(ancestor::dl:field_item) = 1])"/>
|
|
326
|
+
<sch:assert test="$own-key-count le 1">
|
|
327
|
+
A field_item may contain at most one own descendant key.
|
|
328
|
+
Keys that belong to nested field_item descendants are excluded from this count.
|
|
329
|
+
Found own-key-count=<sch:value-of select="$own-key-count"/>.
|
|
330
|
+
</sch:assert>
|
|
331
|
+
</sch:rule>
|
|
332
|
+
</sch:pattern>
|
|
333
|
+
|
|
334
|
+
<!-- ============================================ -->
|
|
335
|
+
<!-- PICTURE BODY: table only as first body element when class="chart" -->
|
|
336
|
+
<!-- ============================================ -->
|
|
337
|
+
|
|
338
|
+
<sch:pattern id="picture-body">
|
|
339
|
+
<sch:rule context="dl:picture">
|
|
340
|
+
<sch:let name="first-body" value="*[not(self::dl:label or self::dl:thread or self::dl:xref or self::dl:href or self::dl:location or self::dl:caption or self::dl:custom)][1]"/>
|
|
341
|
+
|
|
342
|
+
<sch:assert test="not(not(@class) or @class = 'undefined') or empty(dl:table)">
|
|
343
|
+
Picture with class="undefined" (or no class) must not contain table in the element body.
|
|
344
|
+
</sch:assert>
|
|
345
|
+
|
|
346
|
+
<sch:assert test="empty(dl:table) or (@class = 'chart' and dl:table[1] is $first-body)">
|
|
347
|
+
Element table is only allowed as the first element of the body of picture with class="chart".
|
|
348
|
+
</sch:assert>
|
|
349
|
+
</sch:rule>
|
|
350
|
+
</sch:pattern>
|
|
351
|
+
|
|
352
|
+
</sch:schema>
|