diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 43ad16fa..bb4d6f8d 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -69,6 +69,7 @@ jobs:
pytest tests/test_manifests.py --tests=./specifications/json-ld-api/tests --loader=${{ matrix.loader }}
pytest tests/test_manifests.py --tests=./specifications/json-ld-framing/tests --loader=${{ matrix.loader }}
pytest tests/test_manifests.py --tests=./specifications/normalization/tests --loader=${{ matrix.loader }}
+ pytest tests/test_manifests.py --tests=./specifications/yaml-ld/tests --loader=${{ matrix.loader }}
pytest --ignore ./tests/test_manifests.py
env:
LOADER: ${{ matrix.loader }}
diff --git a/.gitignore b/.gitignore
index 820ee873..9ffb4bc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,6 @@ tests/data/test_caching.json
# Local version file for pyenv
.python-version
+
+# Local lock file for uv
+uv.lock
diff --git a/.gitmodules b/.gitmodules
index f4e5432a..6c7ecd1d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,3 +7,6 @@
[submodule "specifications/normalization"]
path = specifications/normalization
url = https://github.com/json-ld/normalization.git
+[submodule "specifications/yaml-ld"]
+ path = specifications/yaml-ld
+ url = https://github.com/w3c/yaml-ld.git
diff --git a/README.md b/README.md
index d38ad939..cd3fd58f 100644
--- a/README.md
+++ b/README.md
@@ -331,13 +331,14 @@ git submodule update
#### Cloning manually
You can also avoid using git submodules by manually cloning the `json-ld-api`,
-`json-ld-framing`, and `normalization` repositories hosted on GitHub using the
-following commands:
+`json-ld-framing`, `normalization`, and `yaml-ld` repositories hosted on GitHub
+using the following commands:
```bash
git clone https://github.com/w3c/json-ld-api ./specifications/json-ld-api
git clone https://github.com/w3c/json-ld-framing ./specifications/json-ld-framing
git clone https://github.com/json-ld/normalization ./specifications/normalization
+git clone https://github.com/w3c/yaml-ld ./specifications/yaml-ld
```
Note that you can clone these repositories into any location you wish; however,
diff --git a/docs/project/decisions/choose-where-to-host-yaml-ld-support.md b/docs/project/decisions/choose-where-to-host-yaml-ld-support.md
new file mode 100644
index 00000000..9851a5a4
--- /dev/null
+++ b/docs/project/decisions/choose-where-to-host-yaml-ld-support.md
@@ -0,0 +1,81 @@
+---
+title: Treat YAML-LD support as …
+status: undecided
+date: 2026-07-19
+author: Anatoly Scherbakov
+tags: [decision]
+hide: [toc]
+---
+
+# Treat YAML-LD support as …
+
+{{ adr_metadata(date, status) }}
+
+## :material-text-box-outline: Context
+
+At the moment of writing this document, the [JSON-LD Working Group](https://www.w3.org/groups/wg/json-ld/) is preparing a new specification, [YAML-LD](https://www.w3.org/TR/yaml-ld-10/), for the status of [Recommendation](https://www.w3.org/standards/types/#x2-5-recommendation). It [leverages the readability and conciseness of YAML, but relies on the JSON-LD data model and API](https://www.w3.org/TR/yaml-ld-10/). Here is an example:
+
+{{ source_file('project/decisions/examples/intro.yamlld') }}
+
+The specification is already stable in the normative sense, and the Working Group is inviting implementations for the new standard.
+
+Should we provide such an implementation for Python within [:fontawesome-brands-github: `digitalbazaar/pyld`](https://github.com/digitalbazaar/pyld)?
+
+## :material-arrow-decision-outline: Decision
+
+
+
+ |
+ PyLD implements YAML-LD |
+ `python-yaml-ld` implements YAML-LD |
+ PyLD is a façade on top of `python-yaml-ld` |
+
+
+ | Install |
+ `pip install 'PyLD[yaml-ld]'` |
+ `pip install yaml-ld` |
+ `pip install 'PyLD[yaml-ld]'` |
+
+
+ | Dependencies |
+ YAML parser, for instance: `ruamel.yaml` or `pyyaml` |
+ [PyLD](https://github.com/iolanta-tech/python-yaml-ld/blob/master/pyproject.toml) |
+ `PyLD` → `yaml-ld` → `PyLD` |
+
+
+ | Test suite |
+ PyLD (`specifications/yaml-ld`) |
+ `yaml-ld` |
+ `yaml-ld` |
+
+
+ | YAML parsing |
+ `PyLD` |
+ `yaml-ld` |
+ `yaml-ld` |
+
+
+ | How to expand() |
+ `pyld.jsonld.expand()` |
+ `yaml_ld.expand()` |
+ `pyld.jsonld.expand()` |
+
+
+ | Decision |
+ :question: |
+ :question: |
+ :x: Circular dependency (`PyLD` → `yaml-ld` → `PyLD`) |
+
+
+
+## :material-arrow-right-bold-outline: Consequences
+
+- Docs, optional extras, and ownership of YAML-LD conformance must match the chosen packaging home.
+- Dependency surface (YAML parser and any optional extras) will either grow in PyLD or stay in a separate package.
+- Release cadence for YAML-LD support fixes will either couple to PyLD releases or remain with a separate package.
+
+#### Implementation Steps
+
+- [ ] Record the Decision row outcome in this ADR (flip status to decided)
+- [ ] Align suite location, YAML parsing ownership, `expand()` entry point, and install surface with the chosen column
+- [ ] Update project index / user docs if needed
diff --git a/docs/project/decisions/examples/intro.yamlld b/docs/project/decisions/examples/intro.yamlld
new file mode 100644
index 00000000..e7f685d0
--- /dev/null
+++ b/docs/project/decisions/examples/intro.yamlld
@@ -0,0 +1,19 @@
+"@context":
+ - https://json-ld.org/contexts/dollar-convenience.jsonld
+ - schema: https://schema.org/
+ dbo: http://dbpedia.org/ontology/
+ dbp: http://dbpedia.org/property/
+ dbr: http://dbpedia.org/resource/
+ xsd: http://www.w3.org/2001/XMLSchema#
+ dbp:discovered:
+ "@type": xsd:date
+ dbp:star:
+ "@type": "@id"
+
+$id: dbr:Proxima_Centauri_b
+$type: dbo:Planet
+schema:description: >-
+ The closest known exoplanet to Earth,
+ orbiting in Proxima Centauri's habitable zone.
+dbp:discovered: 2016-08-24
+dbp:star: dbr:Proxima_Centauri
diff --git a/docs/project/index.md b/docs/project/index.md
index e42be8e9..aeeeb62c 100644
--- a/docs/project/index.md
+++ b/docs/project/index.md
@@ -12,6 +12,12 @@ Architecture Decision Records (ADRs) document the technical choices taken during
+!!! warning inline "[Treat YAML-LD support as …](decisions/choose-where-to-host-yaml-ld-support/)"
+ :material-calendar-clock: 19 July 2026
+
+ Bundle into PyLD, keep a separate package (with suite under `specifications/`),
+ expose a thin PyLD facade, or treat YAML-LD as out of scope (no suite either).
+
!!! success inline "[Use `requests-cache` for persistent HTTP caching in synchronous Python code](decisions/use-requests-cache-for-sync-http-caching-in-document-loaders/)"
:material-calendar-clock: 29 June 2026
diff --git a/docs_macros.py b/docs_macros.py
index 1a3c3643..99b84d22 100644
--- a/docs_macros.py
+++ b/docs_macros.py
@@ -11,14 +11,25 @@
from yaml import SafeLoader
ROOT_DIR = Path(__file__).resolve().parent
-EXAMPLES_DIR = ROOT_DIR / 'docs' / 'examples'
+DOCS_DIR = ROOT_DIR / 'docs'
+EXAMPLES_DIR = DOCS_DIR / 'examples'
sys.path.insert(0, str(ROOT_DIR / 'lib'))
sys.path.insert(0, str(ROOT_DIR / 'tests'))
+_SOURCE_LANG = {
+ '.yamlld': 'yaml',
+ '.yaml': 'yaml',
+ '.yml': 'yaml',
+ '.jsonld': 'json',
+ '.json': 'json',
+ '.py': 'python',
+}
+
MANIFEST_BASES = {
'frame-manifest': 'https://w3c.github.io/json-ld-framing/tests',
'manifest-urgna2012': 'https://w3c.github.io/rdf-canon/tests',
'manifest-urdna2015': 'https://w3c.github.io/rdf-canon/tests',
+ 'manifest': 'https://w3c.github.io/yaml-ld/tests',
}
DEFAULT_TEST_BASE = 'https://w3c.github.io/json-ld-api/tests'
@@ -26,6 +37,7 @@
_MANIFEST_PATHS = (
ROOT_DIR / 'specifications' / 'json-ld-api' / 'tests',
ROOT_DIR / 'specifications' / 'json-ld-framing' / 'tests',
+ ROOT_DIR / 'specifications' / 'yaml-ld' / 'tests',
)
@@ -33,12 +45,19 @@ def _parse_skip_id_regex(pattern):
match = _SKIP_ID_PATTERN.fullmatch(pattern)
if not match:
return None
- return match.group('manifest'), match.group('test_id')
+ # Suite-scoped patterns may include a path prefix, e.g.
+ # .*yaml-ld/tests/manifest#two-documents-from-stream$
+ manifest = match.group('manifest')
+ if '/' in manifest:
+ manifest = manifest.rsplit('/', 1)[-1]
+ return manifest, match.group('test_id')
def _test_url(manifest, test_id):
- base = MANIFEST_BASES.get(manifest, DEFAULT_TEST_BASE)
- return f'{base}/{manifest}.html#{test_id}'
+ # Basename only — suite-scoped regex captures may still contain '/'.
+ manifest_name = manifest.rsplit('/', 1)[-1]
+ base = MANIFEST_BASES.get(manifest_name, DEFAULT_TEST_BASE)
+ return f'{base}/{manifest_name}.html#{test_id}'
def _jsonld_values(data, key):
@@ -59,7 +78,11 @@ def _manifest_entries():
for manifest_dir in _MANIFEST_PATHS:
if not manifest_dir.exists():
continue
- for path in sorted(manifest_dir.glob('*-manifest.jsonld')):
+ paths = {
+ *manifest_dir.glob('*-manifest.jsonld'),
+ *manifest_dir.glob('manifest.jsonld'),
+ }
+ for path in sorted(paths):
data = json.loads(path.read_text())
manifest = path.stem
for entry in _jsonld_values(data, 'sequence'):
@@ -82,6 +105,11 @@ def _skip_reason(test_type, skip, test):
for pattern in skip.get('idRegex', []):
if re.match(pattern, test_id):
return f'Explicit skip (`{test_type}`)'
+ # Suite-scoped patterns (e.g. .*yaml-ld/tests/manifest#id$) still
+ # refer to the same short manifest#id used in local entries.
+ parsed = _parse_skip_id_regex(pattern)
+ if parsed and f'{parsed[0]}#{parsed[1]}' == test_id:
+ return f'Explicit skip (`{test_type}`)'
for pattern in skip.get('descriptionRegex', []):
if re.match(pattern, entry.get('description', '')):
@@ -114,6 +142,13 @@ def _example_path(name):
return path
+def _docs_path(rel):
+ path = (DOCS_DIR / rel).resolve()
+ if not path.is_relative_to(DOCS_DIR.resolve()):
+ raise ValueError(f'Invalid docs path: {rel}')
+ return path
+
+
def _github_branch():
branch = os.environ.get('GITHUB_REF_NAME')
if branch:
@@ -129,10 +164,20 @@ def _github_branch():
return 'master'
-def _example_github_url(name, repo_url):
- rel_path = Path('docs/examples') / name
+def _docs_github_url(rel, repo_url):
branch = 'master'
- return f'{repo_url.rstrip("/")}/blob/{branch}/{rel_path.as_posix()}'
+ return f'{repo_url.rstrip("/")}/blob/{branch}/docs/{Path(rel).as_posix()}'
+
+
+def _example_github_url(name, repo_url):
+ return _docs_github_url(Path('examples') / name, repo_url)
+
+
+def _example_admonition(title, body, indent=0):
+ content_indent = indent + 4
+ pad = ' ' * content_indent
+ indented = '\n'.join(f'{pad}{line}' for line in body.splitlines())
+ return f'!!! example "{title}"\n\n{indented}\n'
def _human_date(value):
@@ -319,7 +364,19 @@ def example(name, output_syntax=None, indent=0):
f'```python\n{source}```\n\n'
f'```{output_lang} title="Output"\n{result.stdout}```'
)
- content_indent = indent + 4
- pad = ' ' * content_indent
- indented = '\n'.join(f'{pad}{line}' for line in body.splitlines())
- return f'!!! example "{title}"\n\n{indented}\n'
+ return _example_admonition(title, body, indent=indent)
+
+ @env.macro
+ def source_file(rel, indent=0):
+ """Include a docs file in an example admonition with a GitHub source link."""
+ path = _docs_path(rel)
+ source = path.read_text()
+ github_url = _docs_github_url(rel, env.conf['repo_url'])
+ lang = _SOURCE_LANG.get(path.suffix.lower(), 'text')
+ title = (
+ f'Example'
+ f':fontawesome-brands-github: [`{path.name}`]({github_url})'
+ f''
+ )
+ body = f'```{lang}\n{source}```'
+ return _example_admonition(title, body, indent=indent)
diff --git a/mkdocs.yml b/mkdocs.yml
index 4d2a59db..340dedad 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -34,6 +34,10 @@ markdown_extensions:
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
+ - pymdownx.snippets:
+ base_path:
+ - docs
+ check_paths: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
diff --git a/specifications/yaml-ld b/specifications/yaml-ld
new file mode 160000
index 00000000..af4c9c8e
--- /dev/null
+++ b/specifications/yaml-ld
@@ -0,0 +1 @@
+Subproject commit af4c9c8eb77454645a0b16e69ec16b28a322ff06
diff --git a/tests/runtests.py b/tests/runtests.py
index 255dc8f9..26b12cb2 100644
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -91,12 +91,14 @@
'https://w3c.github.io/json-ld-api/tests',
'https://w3c.github.io/json-ld-framing/tests',
'https://github.com/json-ld/normalization/tests',
+ 'https://w3c.github.io/yaml-ld/tests',
]
SPEC_DIRS = [
'../specifications/json-ld-api/tests/',
'../specifications/json-ld-framing/tests/',
'../specifications/normalization/tests/',
+ '../specifications/yaml-ld/tests/',
]
# NOTE: The following TestRunner class can be removed because pytest now
@@ -718,6 +720,8 @@ def load_locally(url):
url_no_frag = strip_fragment(url)
if not content_type and url_no_frag.endswith('.jsonld'):
content_type = 'application/ld+json'
+ if not content_type and url_no_frag.endswith('.yamlld'):
+ content_type = 'application/ld+yaml'
if not content_type and url_no_frag.endswith('.json'):
content_type = 'application/json'
if not content_type and url_no_frag.endswith('.html'):
@@ -918,7 +922,11 @@ def write(self, filename):
# skip tests where behavior changed for a 1.1 processor
# see JSON-LD 1.0 Errata
'specVersion': ['json-ld-1.0'],
- 'idRegex': [],
+ # YAML-LD suite: only currently failing cases (passing cases stay active)
+ 'idRegex': [
+ '.*yaml-ld/tests/manifest#compact-local-json-ld-context$',
+ '.*yaml-ld/tests/manifest#compact-local-yaml-ld-context$',
+ ],
},
'fn': 'compact',
'params': [
@@ -950,7 +958,50 @@ def write(self, filename):
# skip tests where behavior changed for a 1.1 processor
# see JSON-LD 1.0 Errata
'specVersion': ['json-ld-1.0'],
- 'idRegex': [],
+ # YAML-LD suite: only currently failing cases (passing cases stay active)
+ 'idRegex': [
+ '.*yaml-ld/tests/manifest#cir-mapping-key-1-negative$',
+ '.*yaml-ld/tests/manifest#cir-mapping-key-2-negative$',
+ '.*yaml-ld/tests/manifest#cir-mapping-key-3-negative$',
+ '.*yaml-ld/tests/manifest#cir-mapping-key-4-negative$',
+ '.*yaml-ld/tests/manifest#cir-mapping-key-5-negative$',
+ '.*yaml-ld/tests/manifest#cir-scalar-core-1-positive$',
+ '.*yaml-ld/tests/manifest#cir-scalar-i18n-1-positive$',
+ '.*yaml-ld/tests/manifest#cir-scalar-other-1-positive$',
+ '.*yaml-ld/tests/manifest#cr-comments-1-positive$',
+ '.*yaml-ld/tests/manifest#cr-utf8-1-positive$',
+ '.*yaml-ld/tests/manifest#cr-utf8-2-negative$',
+ '.*yaml-ld/tests/manifest#cr-well-formed-1-positive$',
+ '.*yaml-ld/tests/manifest#aa-cycles-1-positive$',
+ '.*yaml-ld/tests/manifest#aa-cycles-3-positive$',
+ '.*yaml-ld/tests/manifest#html-and-yaml-streams$',
+ '.*yaml-ld/tests/manifest#mixed-script-types$',
+ '.*yaml-ld/tests/manifest#html-dedent-needed$',
+ '.*yaml-ld/tests/manifest#html-dedent-not-needed$',
+ '.*yaml-ld/tests/manifest#two-documents-from-stream$',
+ '.*yaml-ld/tests/manifest#one-document-from-stream$',
+ '.*yaml-ld/tests/manifest#local-json-ld-context$',
+ '.*yaml-ld/tests/manifest#local-yaml-ld-context$',
+ '.*yaml-ld/tests/manifest#core-bool-true$',
+ '.*yaml-ld/tests/manifest#core-bool-false$',
+ '.*yaml-ld/tests/manifest#core-bool-true-title$',
+ '.*yaml-ld/tests/manifest#core-bool-false-upper$',
+ '.*yaml-ld/tests/manifest#core-null-null$',
+ '.*yaml-ld/tests/manifest#core-null-tilde$',
+ '.*yaml-ld/tests/manifest#core-null-upper$',
+ '.*yaml-ld/tests/manifest#core-int-decimal$',
+ '.*yaml-ld/tests/manifest#core-int-octal$',
+ '.*yaml-ld/tests/manifest#core-int-hex$',
+ '.*yaml-ld/tests/manifest#core-float-basic$',
+ '.*yaml-ld/tests/manifest#core-float-scientific$',
+ '.*yaml-ld/tests/manifest#core-date-ymd$',
+ '.*yaml-ld/tests/manifest#core-yaml11-yes$',
+ '.*yaml-ld/tests/manifest#core-yaml11-no$',
+ '.*yaml-ld/tests/manifest#core-yaml11-on$',
+ '.*yaml-ld/tests/manifest#core-yaml11-off$',
+ '.*yaml-ld/tests/manifest#core-yaml11-yes-title$',
+ '.*yaml-ld/tests/manifest#core-yaml11-yes-upper$',
+ ],
},
'fn': 'expand',
'params': [read_test_url('input'), create_test_options()],
@@ -961,7 +1012,10 @@ def write(self, filename):
# skip tests where behavior changed for a 1.1 processor
# see JSON-LD 1.0 Errata
'specVersion': ['json-ld-1.0'],
- 'idRegex': [],
+ # YAML-LD suite: only currently failing cases (passing cases stay active)
+ 'idRegex': [
+ '.*yaml-ld/tests/manifest#flatten$',
+ ],
},
'fn': 'flatten',
'params': [
@@ -976,7 +1030,10 @@ def write(self, filename):
# skip tests where behavior changed for a 1.1 processor
# see JSON-LD 1.0 Errata
'specVersion': ['json-ld-1.0'],
- 'idRegex': [],
+ # YAML-LD suite: only currently failing cases (passing cases stay active)
+ 'idRegex': [
+ '.*yaml-ld/tests/manifest#frame-t0001$',
+ ],
},
'fn': 'frame',
'params': [
@@ -1026,7 +1083,12 @@ def write(self, filename):
# skip tests where behavior changed for a 1.1 processor
# see JSON-LD 1.0 Errata
'specVersion': ['json-ld-1.0'],
- 'idRegex': [],
+ # YAML-LD suite: only currently failing cases (passing cases stay active)
+ 'idRegex': [
+ '.*yaml-ld/tests/manifest#aa-information-1-positive$',
+ '.*yaml-ld/tests/manifest#cir-scalar-core-2-positive$',
+ '.*yaml-ld/tests/manifest#cir-scalar-other-2-positive$',
+ ],
},
'fn': 'to_rdf',
'params': [