├── .github └── workflows │ ├── add_prs_and_issues_to_project.yml │ └── ci.yml ├── .gitignore ├── .pr-preview.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── common ├── common.js ├── extract-examples.rb └── jsonld.js ├── index.html └── w3c.json /.github/workflows/add_prs_and_issues_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Add pull requests and issues to projects 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | issues: 8 | types: 9 | - opened 10 | 11 | jobs: 12 | add-to-project: 13 | name: Add PR and issues to project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/add-to-project@v0.4.1 17 | with: 18 | project-url: https://github.com/orgs/w3c/projects/84 19 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow validates the document for markup and examples. 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [ '**' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | tests: 12 | name: Build and Validate 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: 3.2 20 | - name: Install dependencies 21 | run: bundle install 22 | 23 | # Validate Examples 24 | - name: Verify examples are consistent 25 | run: bundle exec rake test 26 | 27 | # Validate via ReSpec 28 | # See https://github.com/w3c/spec-prod/blob/main/docs/examples.md 29 | - name: ReSpec Checker 30 | uses: w3c/spec-prod@v2 31 | with: 32 | VALIDATE_LINKS: false 33 | VALIDATE_MARKUP: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.byebug_history 2 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.html", 3 | "type": "respec" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # JSON-LD Working Group 2 | 3 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the 4 | [W3C Patent Policy](https://www.w3.org/Consortium/Patent-Policy-20040205/) and 5 | [Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). To make substantive contributions to specifications, you must either participate 6 | in the relevant W3C Working Group or make a non-member patent licensing commitment. 7 | 8 | If you are not the sole contributor to a contribution (pull request), please identify all 9 | contributors in the pull request comment. 10 | 11 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 12 | 13 | ``` 14 | +@github_username 15 | ``` 16 | 17 | If you added a contributor by mistake, you can remove them in a comment with: 18 | 19 | ``` 20 | -@github_username 21 | ``` 22 | 23 | If you are making a pull request on behalf of someone else but you had no part in designing the 24 | feature, you can remove yourself with the above syntax. 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'json-ld', github: 'ruby-rdf/json-ld', branch: 'develop' 4 | gem 'rdf-isomorphic', github: 'ruby-rdf/rdf-isomorphic', branch: 'develop' 5 | gem 'linkeddata' 6 | gem 'rdf-trig', github: 'ruby-rdf/rdf-trig', branch: 'develop' 7 | gem 'earl-report' 8 | gem 'nokogiri' 9 | gem 'colorize' 10 | gem 'rake' 11 | gem 'redcarpet' 12 | gem 'byebug' 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/ruby-rdf/json-ld.git 3 | revision: fcd3aedd310aedbd61e6e1f1afa1b37d1ffebc2d 4 | branch: develop 5 | specs: 6 | json-ld (3.3.1) 7 | htmlentities (~> 4.3) 8 | json-canonicalization (~> 1.0) 9 | link_header (~> 0.0, >= 0.0.8) 10 | multi_json (~> 1.15) 11 | rack (>= 2.2, < 4) 12 | rdf (~> 3.3) 13 | 14 | GIT 15 | remote: https://github.com/ruby-rdf/rdf-isomorphic.git 16 | revision: 6add041cd0be0ff402285282d7bd4c784e2c89d2 17 | branch: develop 18 | specs: 19 | rdf-isomorphic (3.3.0) 20 | rdf (~> 3.3) 21 | 22 | GIT 23 | remote: https://github.com/ruby-rdf/rdf-trig.git 24 | revision: ccb51eff7022697ada2b30e213f50d667307469d 25 | branch: develop 26 | specs: 27 | rdf-trig (3.3.0) 28 | ebnf (~> 2.4) 29 | rdf (~> 3.3) 30 | rdf-turtle (~> 3.3) 31 | 32 | GEM 33 | remote: https://rubygems.org/ 34 | specs: 35 | addressable (2.8.6) 36 | public_suffix (>= 2.0.2, < 6.0) 37 | bcp47_spec (0.2.1) 38 | builder (3.2.4) 39 | byebug (11.1.3) 40 | colorize (1.1.0) 41 | concurrent-ruby (1.2.3) 42 | connection_pool (2.4.1) 43 | earl-report (0.9.0) 44 | haml (>= 6.1) 45 | json-ld (~> 3.3) 46 | kramdown (~> 2.4) 47 | rdf (~> 3.3) 48 | rdf-turtle (~> 3.3) 49 | rdf-vocab (~> 3.3) 50 | sparql (~> 3.3) 51 | ebnf (2.4.0) 52 | htmlentities (~> 4.3) 53 | rdf (~> 3.3) 54 | scanf (~> 1.0) 55 | sxp (~> 1.3) 56 | unicode-types (~> 1.8) 57 | haml (6.3.0) 58 | temple (>= 0.8.2) 59 | thor 60 | tilt 61 | hamster (3.0.0) 62 | concurrent-ruby (~> 1.0) 63 | htmlentities (4.3.4) 64 | json-canonicalization (1.0.0) 65 | json-ld-preloaded (3.3.0) 66 | json-ld (~> 3.3) 67 | rdf (~> 3.3) 68 | kramdown (2.4.0) 69 | rexml 70 | ld-patch (3.3.0) 71 | ebnf (~> 2.4) 72 | rdf (~> 3.3) 73 | rdf-xsd (~> 3.3) 74 | sparql (~> 3.3) 75 | sxp (~> 1.3) 76 | link_header (0.0.8) 77 | linkeddata (3.3.1) 78 | json-ld (~> 3.3) 79 | json-ld-preloaded (~> 3.3) 80 | ld-patch (~> 3.3) 81 | nokogiri (~> 1.15, >= 1.15.4) 82 | rdf (~> 3.2, >= 3.2.1) 83 | rdf-aggregate-repo (~> 3.2) 84 | rdf-hamster-repo (~> 3.3) 85 | rdf-isomorphic (~> 3.3) 86 | rdf-json (~> 3.3) 87 | rdf-microdata (~> 3.3) 88 | rdf-n3 (~> 3.3) 89 | rdf-normalize (~> 0.7) 90 | rdf-ordered-repo (~> 3.3) 91 | rdf-rdfa (~> 3.3) 92 | rdf-rdfxml (~> 3.3) 93 | rdf-reasoner (~> 0.9) 94 | rdf-tabular (~> 3.3) 95 | rdf-trig (~> 3.3) 96 | rdf-trix (~> 3.3) 97 | rdf-turtle (~> 3.3) 98 | rdf-vocab (~> 3.3) 99 | rdf-xsd (~> 3.3) 100 | shacl (~> 0.4) 101 | shex (~> 0.8) 102 | sparql (~> 3.3) 103 | sparql-client (~> 3.3) 104 | yaml-ld (~> 0.0) 105 | logger (1.6.0) 106 | matrix (0.4.2) 107 | mini_portile2 (2.8.8) 108 | multi_json (1.15.0) 109 | net-http-persistent (4.0.2) 110 | connection_pool (~> 2.2) 111 | nokogiri (1.18.8) 112 | mini_portile2 (~> 2.8.2) 113 | racc (~> 1.4) 114 | nokogiri (1.18.8-aarch64-linux-gnu) 115 | racc (~> 1.4) 116 | nokogiri (1.18.8-arm-linux-gnu) 117 | racc (~> 1.4) 118 | nokogiri (1.18.8-arm64-darwin) 119 | racc (~> 1.4) 120 | nokogiri (1.18.8-x86_64-darwin) 121 | racc (~> 1.4) 122 | nokogiri (1.18.8-x86_64-linux-gnu) 123 | racc (~> 1.4) 124 | psych (5.1.2) 125 | stringio 126 | public_suffix (5.0.4) 127 | racc (1.8.1) 128 | rack (3.0.16) 129 | rake (13.1.0) 130 | rdf (3.3.1) 131 | bcp47_spec (~> 0.2) 132 | link_header (~> 0.0, >= 0.0.8) 133 | rdf-aggregate-repo (3.3.0) 134 | rdf (~> 3.3) 135 | rdf-hamster-repo (3.3.0) 136 | hamster (~> 3.0) 137 | rdf (~> 3.3) 138 | rdf-json (3.3.0) 139 | rdf (~> 3.3) 140 | rdf-microdata (3.3.0) 141 | htmlentities (~> 4.3) 142 | nokogiri (~> 1.15, >= 1.15.4) 143 | rdf (~> 3.3) 144 | rdf-rdfa (~> 3.3) 145 | rdf-xsd (~> 3.3) 146 | rdf-n3 (3.3.0) 147 | ebnf (~> 2.4) 148 | rdf (~> 3.3) 149 | sparql (~> 3.3) 150 | sxp (~> 1.3) 151 | rdf-normalize (0.7.0) 152 | rdf (~> 3.3) 153 | rdf-ordered-repo (3.3.0) 154 | rdf (~> 3.3) 155 | rdf-rdfa (3.3.0) 156 | haml (~> 6.1) 157 | htmlentities (~> 4.3) 158 | rdf (~> 3.3) 159 | rdf-aggregate-repo (~> 3.3) 160 | rdf-vocab (~> 3.3) 161 | rdf-xsd (~> 3.3) 162 | rdf-rdfxml (3.3.0) 163 | builder (~> 3.2, >= 3.2.4) 164 | htmlentities (~> 4.3) 165 | rdf (~> 3.3) 166 | rdf-xsd (~> 3.3) 167 | rdf-reasoner (0.9.0) 168 | rdf (~> 3.3) 169 | rdf-xsd (~> 3.3) 170 | rdf-tabular (3.3.0) 171 | addressable (~> 2.8) 172 | bcp47_spec (~> 0.2) 173 | json-ld (~> 3.3) 174 | rdf (~> 3.3) 175 | rdf-vocab (~> 3.3) 176 | rdf-xsd (~> 3.3) 177 | rdf-trix (3.3.0) 178 | rdf (~> 3.3) 179 | rdf-xsd (~> 3.3) 180 | rdf-turtle (3.3.0) 181 | ebnf (~> 2.4) 182 | rdf (~> 3.3) 183 | rdf-vocab (3.3.0) 184 | rdf (~> 3.3) 185 | rdf-xsd (3.3.0) 186 | rdf (~> 3.3) 187 | rexml (~> 3.2) 188 | redcarpet (3.6.0) 189 | rexml (3.3.9) 190 | scanf (1.0.0) 191 | shacl (0.4.1) 192 | json-ld (~> 3.3) 193 | rdf (~> 3.3) 194 | sparql (~> 3.3) 195 | sxp (~> 1.2) 196 | shex (0.8.0) 197 | ebnf (~> 2.4) 198 | htmlentities (~> 4.3) 199 | json-ld (~> 3.3) 200 | json-ld-preloaded (~> 3.3) 201 | rdf (~> 3.3) 202 | rdf-xsd (~> 3.3) 203 | sparql (~> 3.3) 204 | sxp (~> 1.3) 205 | sparql (3.3.0) 206 | builder (~> 3.2, >= 3.2.4) 207 | ebnf (~> 2.4) 208 | logger (~> 1.5) 209 | rdf (~> 3.3) 210 | rdf-aggregate-repo (~> 3.3) 211 | rdf-xsd (~> 3.3) 212 | sparql-client (~> 3.3) 213 | sxp (~> 1.3) 214 | sparql-client (3.3.0) 215 | net-http-persistent (~> 4.0, >= 4.0.2) 216 | rdf (~> 3.3) 217 | stringio (3.1.0) 218 | sxp (1.3.0) 219 | matrix (~> 0.4) 220 | rdf (~> 3.3) 221 | temple (0.10.3) 222 | thor (1.3.0) 223 | tilt (2.3.0) 224 | unicode-types (1.9.0) 225 | yaml-ld (0.0.3) 226 | json-ld (~> 3.3) 227 | psych (>= 3.3) 228 | rdf (~> 3.3) 229 | rdf-xsd (~> 3.3) 230 | 231 | PLATFORMS 232 | aarch64-linux 233 | arm-linux 234 | arm64-darwin 235 | x86-linux 236 | x86_64-darwin 237 | x86_64-linux 238 | 239 | DEPENDENCIES 240 | byebug 241 | colorize 242 | earl-report 243 | json-ld! 244 | linkeddata 245 | nokogiri 246 | rake 247 | rdf-isomorphic! 248 | rdf-trig! 249 | redcarpet 250 | 251 | BUNDLED WITH 252 | 2.5.6 253 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All documents in this Repository are licensed by contributors 2 | under the 3 | [W3C Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![W3C Logo](https://www.w3.org/Icons/w3c_home) 3 | 4 | # JSON-LD 1.1 Best Practices 5 | 6 | This is the repository of the Best Practices document for the W3C’s specification on JSON-LD 1.1, developed by the [JSON-LD Working Group](https://www.w3.org/2018/json-ld-wg/). The editors’ draft of the Note can also be [read directly](https://w3c.github.io/json-ld-bp/). 7 | 8 | ## Contributing to the Repository 9 | 10 | Use the standard fork, branch, and pull request workflow to propose changes to the specification. Please make branch names informative—by including the issue or bug number for example. 11 | 12 | Editorial changes that improve the readability of the spec or correct spelling or grammatical mistakes are welcome. 13 | 14 | Please read [CONTRIBUTING.md](CONTRIBUTING.md), about licensing contributions. 15 | 16 | 17 | ## Code of Conduct 18 | 19 | W3C functions under a [code of conduct](https://www.w3.org/Consortium/cepc/). 20 | ) 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | task default: :test 3 | 4 | desc "Test examples in spec files" 5 | task :test do 6 | sh %(bundle exec common/extract-examples.rb index.html) 7 | end 8 | 9 | desc "Extract Examples" 10 | task :examples do 11 | sh %(rm -rf examples yaml) 12 | sh %(bundle exec common/extract-examples.rb --example-dir examples --yaml-dir yaml index.html) 13 | end 14 | 15 | desc "Check HTML" 16 | task :check_html do 17 | require 'nokogiri' 18 | doc = ::Nokogiri::HTML5(File.open("index.html"), max_parse_errors: 1000) 19 | unless doc.errors.empty? 20 | STDERR.puts "Errors found parsing index.html:" 21 | doc.errors.each {|e| STDERR.puts " #{e}"} 22 | exit(1) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /common/common.js: -------------------------------------------------------------------------------- 1 | /* globals require */ 2 | /* JSON-LD Working Group common spec JavaScript */ 3 | 4 | /* 5 | * Implement tabbed examples. 6 | */ 7 | require(["core/pubsubhub"], (respecEvents) => { 8 | "use strict"; 9 | 10 | respecEvents.sub('end-all', (documentElement) => { 11 | // remove data-cite on where the citation is to ourselves. 12 | const selfDfns = Array.from(document.querySelectorAll("dfn[data-cite^='__SPEC__#']")); 13 | for (const dfn of selfDfns) { 14 | const anchor = dfn.querySelector('a'); 15 | if (anchor) { 16 | const anchorContent = anchor.textContent; 17 | dfn.removeChild(anchor); 18 | dfn.textContent = anchorContent; 19 | } 20 | delete dfn.dataset.cite; 21 | } 22 | 23 | // Update data-cite references to ourselves. 24 | const selfRefs = document.querySelectorAll("a[data-cite^='__SPEC__#']"); 25 | for (const anchor of selfRefs) { 26 | anchor.href= anchor.dataset.cite.replace(/^.*#/,"#"); 27 | delete anchor.dataset.cite; 28 | } 29 | 30 | // 31 | // Remove/hide definitions which are unused 32 | // 1. Find all definitions in a termlist which are not preserved, indexed by data-cite 33 | // 2. Find all references to definitions not in termlist 34 | // 4. Hide definitions which are unreferenced 35 | // 36 | const remoteDfns = []; 37 | document.querySelectorAll(".termlist dfn:not(.preserve)") 38 | .forEach((item, index) => { 39 | if (!selfDfns.includes(item)) { 40 | remoteDfns[item.dataset["cite"]] = item; 41 | } 42 | }); 43 | 44 | // termlist internal references to definitions 45 | const internalRefs = Array.from(document.querySelectorAll(".termlist a[data-cite]")); 46 | 47 | // all references to definitions which are not internal refs 48 | const allRefs = Array.from(document.querySelectorAll("a[data-cite]")) 49 | .filter(e => !internalRefs.includes(e)); 50 | 51 | // Remove terms which are referenced 52 | for (const item of allRefs) { 53 | const cite = item.dataset["cite"]; 54 | // Delete this from remoteDfns, as it is referenced 55 | delete remoteDfns[cite]; 56 | } 57 | 58 | // Now remoteDfns only contains unreferenced terms 59 | for (const item of Object.values(remoteDfns)) { 60 | const dt = item.closest("dt"); 61 | if(dt) { 62 | const dd = dt.nextElementSibling; 63 | // Note, removing messes up some ReSpec references, so hiding instead 64 | // dt.parentNode.removeChild(dt); 65 | // dd.parentNode.removeChild(dd); 66 | dt.hidden = true; 67 | dd.hidden = true; 68 | } 69 | } 70 | 71 | // 72 | // Playground 73 | // 74 | 75 | // Add playground links 76 | for (const link of document.querySelectorAll("a.playground")) { 77 | let pre; 78 | if (link.dataset.resultFor) { 79 | // Referenced pre element 80 | pre = document.querySelector(link.dataset.resultFor + ' > pre'); 81 | } else { 82 | // First pre element of aside 83 | pre = link.closest("aside").querySelector("pre"); 84 | } 85 | const content = unComment(document, pre.textContent) 86 | .replace(/\*\*\*\*/g, '') 87 | .replace(/####([^#]*)####/g, ''); 88 | link.setAttribute('aria-label', 'playground link'); 89 | link.textContent = "Open in playground"; 90 | 91 | // startTab defaults to "expand" 92 | const linkQueryParams = { 93 | startTab: "tab-expand", 94 | "json-ld": content 95 | } 96 | 97 | if (link.dataset.compact !== undefined) { 98 | linkQueryParams.startTab = "tab-" + "compacted"; 99 | linkQueryParams.context = '{}'; 100 | } 101 | 102 | if (link.dataset.flatten !== undefined) { 103 | linkQueryParams.startTab = "tab-" + "flattened"; 104 | linkQueryParams.context = '{}'; 105 | } 106 | 107 | if (link.dataset.frame !== undefined) { 108 | linkQueryParams.startTab = "tab-" + "framed"; 109 | const frameContent = unComment(document, document.querySelector(link.dataset.frame + ' > pre').textContent) 110 | .replace(/\*\*\*\*/g, '') 111 | .replace(/####([^#]*)####/g, ''); 112 | linkQueryParams.frame = frameContent; 113 | } 114 | 115 | // Set context 116 | if (link.dataset.context) { 117 | const contextContent = unComment(document, document.querySelector(link.dataset.context + ' > pre').textContent) 118 | .replace(/\*\*\*\*/g, '') 119 | .replace(/####([^#]*)####/g, ''); 120 | linkQueryParams.context = contextContent; 121 | } 122 | 123 | link.setAttribute('href', 124 | 'https://json-ld.org/playground/#' + 125 | Object.keys(linkQueryParams).map(k => `${encodeURIComponent(k)}=${encodeURIComponent(linkQueryParams[k])}`) 126 | .join('&')); 127 | } 128 | 129 | // Add highlighting and remove comment from pre elements 130 | for (const pre of document.querySelectorAll("pre")) { 131 | // First pre element of aside 132 | const content = pre.innerHTML 133 | .replace(/\*\*\*\*([^*]*)\*\*\*\*/g, '$1') 134 | .replace(/####([^#]*)####/g, '$1'); 135 | pre.innerHTML = content; 136 | } 137 | }); 138 | }); 139 | 140 | function _esc(s) { 141 | return s.replace(/&/g,'&') 142 | .replace(/>/g,'>') 143 | .replace(/"/g,'"') 144 | .replace(/ s.trim()).map(s => s.search(/[^\s]/)); 154 | const leastIndent = Math.min(...indents); 155 | return lines.map(s => s.slice(leastIndent)).join("\n"); 156 | } 157 | 158 | function updateExample(doc, content) { 159 | // perform transformations to make it render and prettier 160 | return _esc(reindent(unComment(doc, content))); 161 | } 162 | 163 | 164 | function unComment(doc, content) { 165 | // perform transformations to make it render and prettier 166 | return content 167 | .replace(//, '') 169 | .replace(/< !\s*-\s*-/g, '') 171 | .replace(/-\s*-\s*>/g, '-->'); 172 | } 173 | -------------------------------------------------------------------------------- /common/extract-examples.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Extracts examples from a ReSpec document, verifies that example titles are unique. Numbering attempts to replicate that used by ReSpec. Examples in script elements, which are not visibile, may be used for describing the results of related examples 3 | # 4 | # Transformations from JSON-LD 5 | # - @data-frame identifies the title of the frame used to process the example 6 | # - @data-frame-for identifies the source to apply this frame to, verifies that the no errors are encountered 7 | # - @data-context identifies the title of the context used to process the example 8 | # - @data-context-for identifies the source to apply this context to, verifies that the no errors are encountered 9 | # - @data-result-for identifies the title of the source which should result in the content. May be used along with @data-frame or @data-context 10 | # - @data-options indicates the comma-separated option/value pairs to pass to the processor 11 | require 'getoptlong' 12 | require 'json' 13 | require 'json/ld/preloaded' 14 | require 'rdf/isomorphic' 15 | require 'rdf/vocab' 16 | require 'nokogiri' 17 | require 'linkeddata' 18 | require 'fileutils' 19 | require 'colorize' 20 | require 'yaml' 21 | require 'cgi' 22 | 23 | # Define I18N vocabulary 24 | class RDF::Vocab::I18N < RDF::Vocabulary("https://www.w3.org/ns/i18n#"); end unless RDF::Vocab.const_defined?(:I18N) 25 | 26 | # FIXME: This is here until the rdf:JSON is added in RDF.rb 27 | unless RDF::RDFV.properties.include?( RDF.to_uri + 'JSON') 28 | RDF::RDFV.property :JSON, label: "JSON", comment: "JSON datatype" 29 | end 30 | 31 | PREFIXES = { 32 | dc: "http://purl.org/dc/terms/", 33 | dct: "http://purl.org/dc/terms/", 34 | dcterms:"http://purl.org/dc/terms/", 35 | dc11: "http://purl.org/dc/elements/1.1/", 36 | dce: "http://purl.org/dc/elements/1.1/", 37 | cred: "https://w3id.org/credentials#", 38 | ex: "http://example.org/", 39 | foaf: "http://xmlns.com/foaf/0.1/", 40 | prov: "http://www.w3.org/ns/prov#", 41 | rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 42 | schema: "http://schema.org/", 43 | xsd: "http://www.w3.org/2001/XMLSchema#" 44 | } 45 | example_dir = yaml_dir = verbose = number = line = nil 46 | 47 | opts = GetoptLong.new( 48 | ["--example-dir", GetoptLong::REQUIRED_ARGUMENT], 49 | ["--yaml-dir", GetoptLong::REQUIRED_ARGUMENT], 50 | ["--verbose", '-v', GetoptLong::NO_ARGUMENT], 51 | ["--number", '-n', GetoptLong::REQUIRED_ARGUMENT], 52 | ["--line", '-l', GetoptLong::REQUIRED_ARGUMENT], 53 | ) 54 | opts.each do |opt, arg| 55 | case opt 56 | when '--example-dir' then example_dir = arg && FileUtils::mkdir_p(arg) 57 | when '--yaml-dir' then yaml_dir = arg && FileUtils::mkdir_p(arg) 58 | when '--verbose' then verbose = true 59 | when '--number' then number = arg.to_i 60 | when '--line' then line = arg.to_i 61 | end 62 | end 63 | 64 | num_errors = 0 65 | 66 | # Justify and remove leading and trailing blank lines from str 67 | # Remove highlighting and commented out sections 68 | def justify(str) 69 | str = str. 70 | gsub(/^\s*\s*$/, ''). 72 | gsub('****', ''). 73 | gsub(/####([^#]*)####/, '') 74 | 75 | # remove blank lines 76 | lines = str.split("\n").reject {|s| s =~ /\A\s*\z/} 77 | 78 | # count minimum leading space 79 | leading = lines.map {|s| s.length - s.lstrip.length}.min 80 | 81 | # remove leading blank space 82 | lines.map {|s| s[leading..-1]}.join("\n") 83 | end 84 | 85 | def table_to_dataset(table) 86 | repo = RDF::Repository.new 87 | titles = table.xpath('thead/tr/th/text()').map(&:to_s) 88 | 89 | table.xpath('tbody/tr').each do |row| 90 | gname, subject, predicate, object = nil 91 | row.xpath('td/text()').map(&:to_s).each_with_index do |cell, ndx| 92 | case titles[ndx] 93 | when 'Graph' 94 | gname = case cell 95 | when nil, '', " " then nil 96 | when /^_:/ then RDF::Node.intern(cell[2..-1]) 97 | else RDF::Vocabulary.expand_pname(cell) 98 | end 99 | when 'Subject' 100 | subject = case cell 101 | when /^_:/ then RDF::Node.intern(cell[2..-1]) 102 | else RDF::Vocabulary.expand_pname(cell) 103 | end 104 | when 'Property' 105 | predicate = RDF::Vocabulary.expand_pname(cell.sub("dcterms:", "dc:")) 106 | when 'Value' 107 | object = case cell 108 | when /^_:/ then RDF::Node.intern(cell[2..-1]) 109 | when /^\w+:/ then RDF::Vocabulary.expand_pname(cell.sub("dcterms:", "dc:")) 110 | else RDF::Literal(cell) 111 | end 112 | when 'Value Type' 113 | case cell 114 | when /IRI/, '-', /^\s*$/, " " 115 | else 116 | # We might think something was an IRI, but determine that it's not 117 | dt = RDF::Vocabulary.expand_pname(cell.sub("dcterms:", "dc:")) 118 | object = RDF::Literal(object.to_s, datatype: dt) 119 | end 120 | when 'Language' 121 | case cell 122 | when '-', /^\s*$/ 123 | else 124 | # We might think something was an IRI, but determine that it's not 125 | object = RDF::Literal(object.to_s, language: cell.to_sym) 126 | end 127 | when 'Direction' 128 | case cell 129 | when '-', /^\s*$/ 130 | else 131 | object = RDF::Literal(object.to_s, datatype: RDF::URI("https://www.w3.org/ns/i18n##{object.language}_#{cell}")) 132 | # We might think something was an IRI, but determine that it's not 133 | end 134 | end 135 | end 136 | repo << RDF::Statement.new(subject, predicate, object, graph_name: gname) 137 | end 138 | 139 | repo 140 | end 141 | 142 | def dataset_to_table(repo) 143 | has_graph = !repo.graph_names.empty? 144 | litereals = repo.objects.select(&:literal?) 145 | has_datatype = litereals.any?(&:datatype?) 146 | has_language = litereals.any?(&:language?) 147 | positions = {} 148 | 149 | head = [] 150 | head << "Graph" if has_graph 151 | head += %w(Subject Property Value) 152 | 153 | if has_datatype && has_language 154 | head += ["Value Type", "Language"] 155 | positions = {datatype: (has_graph ? 4 : 3), language: (has_graph ? 5 : 4)} 156 | elsif has_datatype 157 | positions = {datatype: (has_graph ? 4 : 3)} 158 | head << "Value Type" 159 | elsif has_language 160 | positions = {language: (has_graph ? 4 : 3)} 161 | head << "Language" 162 | end 163 | 164 | rows = [] 165 | repo.each_statement do |statement| 166 | row = [] 167 | row << (statement.graph_name || " ").to_s if has_graph 168 | row += statement.to_triple.map do |term| 169 | if term.uri? && RDF::Vocabulary.find_term(term) 170 | RDF::Vocabulary.find_term(term).pname.sub("dc:", "dcterms:") 171 | else 172 | term.to_s 173 | end 174 | end 175 | 176 | if has_datatype 177 | if statement.object.literal? && statement.object.datatype? 178 | row[positions[:datatype]] = RDF::Vocabulary.find_term(statement.object.datatype).pname 179 | else 180 | row[positions[:datatype]] = " " 181 | end 182 | end 183 | 184 | if has_language 185 | if statement.object.literal? && statement.object.language? 186 | row[positions[:language]] = statement.object.language.to_s 187 | else 188 | row[positions[:language]] = " " 189 | end 190 | end 191 | 192 | rows << row 193 | end 194 | 195 | "\n " + 196 | head.map {|cell| ""}.join("") + 197 | "\n \n " + 198 | rows.map do |row| 199 | "" + row.map {|cell| ""}.join("") + "" 200 | end.join("\n ") + "\n \n
#{cell}
#{cell}
" 201 | end 202 | 203 | # Allow linting examples 204 | RDF::Reasoner.apply(:rdfs, :schema) 205 | 206 | ARGV.each do |input| 207 | $stderr.puts "\ninput: #{input}" 208 | example_number = 1 # Account for imported Example 1 in typographical conventions 209 | examples = {} 210 | errors = [] 211 | warnings = [] 212 | 213 | File.open(input, "r") do |f| 214 | doc = Nokogiri::HTML.parse(f.read) 215 | doc.css(".example, .illegal-example").each do |element| 216 | error = nil 217 | warn = nil 218 | example_number += 1 if %w(pre aside).include?(element.name) 219 | 220 | if (title = element.attr('title').to_s).empty? 221 | error = "Example #{example_number} at line #{element.line} has no title" 222 | end 223 | 224 | if examples[title] 225 | warn = "Example #{example_number} at line #{element.line} uses duplicate title: #{title}" 226 | end 227 | 228 | def save_example(examples:, element:, title:, example_number:, error:, warn:) 229 | content = justify(element.inner_html) 230 | 231 | ext = case element.attr('data-content-type') 232 | when nil, '', 'application/ld+json' then "jsonld" 233 | when 'application/json' then 'json' 234 | when 'application/n-quads', 'nq' then 'nq' 235 | when 'text/html', 'html' then 'html' 236 | when 'text/turtle', 'ttl' then 'ttl' 237 | when 'application/trig', 'trig' then 'trig' 238 | else 'txt' 239 | end 240 | 241 | # Capture HTML table 242 | if element.name == 'table' 243 | ext, content = 'table', element 244 | end 245 | 246 | fn = "#{title.gsub(/[^\w]+/, '-')}.#{ext}" 247 | examples[title] = { 248 | title: title, 249 | filename: fn, 250 | content: content.to_s.gsub(/^\s*< !\s*-\s*-/, '').gsub(/-\s*-\s*>/, '-->'), 251 | content_type: element.attr('data-content-type'), 252 | number: example_number, 253 | ext: ext, 254 | context_for: element.attr('data-context-for'), 255 | context: element.attr('data-context'), 256 | base: element.attr('data-base'), 257 | ignore: element.attr('data-ignore') || element.attr('class').include?('illegal-example'), 258 | flatten: element.attr('data-flatten'), 259 | compact: element.attr('data-compact'), 260 | fromRdf: element.attr('data-from-rdf'), 261 | toRdf: element.attr('data-to-rdf'), 262 | frame_for: element.attr('data-frame-for'), 263 | no_lint: element.attr('data-no-lint'), 264 | frame: element.attr('data-frame'), 265 | result_for: element.attr('data-result-for'), 266 | options: element.attr('data-options'), 267 | target: element.attr('data-target'), 268 | element: element.name, 269 | line: element.line, 270 | warn: warn, 271 | error: error, 272 | } 273 | #puts "example #{example_number}: #{content}" 274 | end 275 | 276 | if element.name == 'aside' 277 | # If element is aside, look for sub elements with titles 278 | element.css('.original, .compacted, .expanded, .flattened, .turtle, .trig, .statements, .graph, .context, .frame, .framed').each do |sub| 279 | cls = (%w(original compacted expanded flattened turtle trig statements graph context frame) & sub.classes).first 280 | save_example(examples: examples, 281 | element: sub, 282 | title: "#{title}-#{cls}", 283 | example_number: example_number, 284 | error: error, 285 | warn: warn) 286 | end 287 | else 288 | # otherwise, this is the example 289 | save_example(examples: examples, 290 | element: element, 291 | title: title, 292 | example_number: example_number, 293 | error: error, 294 | warn: warn) 295 | end 296 | end 297 | end 298 | 299 | # Process API functions for 300 | examples.values.sort_by {|ex| ex[:number]}.each do |ex| 301 | next if number && number != ex[:number] 302 | next if line && line != ex[:line] 303 | 304 | xpath = '//script[@type="application/ld+json"]' 305 | xpath += %([@id="#{ex[:target][1..-1]}"]) if ex[:target] 306 | args = [] 307 | content = ex[:content] 308 | 309 | options = ex[:options].to_s.split(',').inject({}) do |memo, pair| 310 | k, v = pair.split('=') 311 | v = case v 312 | when 'true' then true 313 | when 'false' then false 314 | else v 315 | end 316 | memo.merge(k.to_sym => v) 317 | end 318 | options[:validate] = true unless options.key?(:validate) 319 | 320 | $stderr.puts "example #{ex[:number]}: #{ex.select{|k,v| k != :content}.to_json(JSON::LD::JSON_STATE)}" if verbose 321 | $stderr.puts "content: #{ex[:content]}" if verbose 322 | 323 | if ex[:ignore] 324 | $stdout.write "i".colorize(:yellow) 325 | next 326 | end 327 | 328 | if ex[:error] 329 | errors << ex[:error] 330 | $stdout.write "F".colorize(:red) 331 | next 332 | end 333 | 334 | if !%w(pre script table).include?(ex[:element]) 335 | errors << "Example #{ex[:number]} at line #{ex[:line]} has unknown element type #{ex[:element]}" 336 | $stdout.write "F".colorize(:red) 337 | next 338 | end 339 | 340 | # Perform example syntactic validation based on extension 341 | case ex[:ext] 342 | when 'json', 'jsonld' 343 | begin 344 | ::JSON.parse(content) 345 | rescue JSON::ParserError => exception 346 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error: #{exception.message}" 347 | $stdout.write "F".colorize(:red) 348 | $stderr.puts exception.backtrace.join("\n") if verbose 349 | next 350 | end 351 | when 'html' 352 | begin 353 | doc = Nokogiri::HTML.parse(content) {|c| c.strict} 354 | doc.errors.each do |er| 355 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error: #{er}" 356 | end 357 | unless doc.errors.empty? 358 | $stdout.write "F".colorize(:red) 359 | next 360 | end 361 | 362 | # Get base from document, if present 363 | html_base = doc.at_xpath('/html/head/base/@href') 364 | ex[:base] = html_base.to_s if html_base 365 | 366 | #script_content = doc.at_xpath(xpath) 367 | #content = script_content.inner_html if script_content 368 | content 369 | rescue Nokogiri::XML::SyntaxError => exception 370 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error: #{exception.message}" 371 | $stdout.write "F".colorize(:red) 372 | $stderr.puts exception.backtrace.join("\n") if verbose 373 | next 374 | end 375 | when 'table' 376 | content = Nokogiri::HTML.parse(content) 377 | when 'ttl', 'trig' 378 | begin 379 | reader_errors = [] 380 | RDF::Repository.new << RDF::TriG::Reader.new(content, logger: reader_errors, **options) 381 | rescue 382 | reader_errors.each do |er| 383 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error: #{er}" 384 | end 385 | $stdout.write "F".colorize(:red) 386 | $stderr.puts $!.backtrace.join("\n") if verbose 387 | next 388 | end 389 | when 'nq' 390 | begin 391 | reader_errors = [] 392 | RDF::Repository.new << RDF::NQuads::Reader.new(content, logger: reader_errors, **options) 393 | rescue 394 | reader_errors.each do |er| 395 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error: #{er}" 396 | end 397 | $stdout.write "F".colorize(:red) 398 | $stderr.puts $!.backtrace.join("\n") if verbose 399 | next 400 | end 401 | end 402 | 403 | if content.is_a?(String) 404 | content = StringIO.new(content) 405 | # Set content_type so it can be parsed properly 406 | content.define_singleton_method(:content_type) {ex[:content_type]} if ex[:content_type] 407 | end 408 | 409 | # Set API to use 410 | method = case 411 | when ex[:compact] then :compact 412 | when ex[:flatten] then :flatten 413 | when ex[:fromRdf] then :fromRdf 414 | when ex[:toRdf] then :toRdf 415 | when ex[:ext] == 'table' then :toRdf 416 | when %w(json ttl trig).include?(ex[:ext] ) 417 | nil 418 | else 419 | :expand 420 | end 421 | 422 | # Set args to parse example content 423 | if ex[:frame_for] 424 | unless examples[ex[:frame_for]] 425 | errors << "Example Frame #{ex[:number]} at line #{ex[:line]} references unknown example #{ex[:frame_for].inspect}" 426 | $stdout.write "F".colorize(:red) 427 | next 428 | end 429 | 430 | method = :frame 431 | args = [StringIO.new(examples[ex[:frame_for]][:content]), content, options] 432 | elsif ex[:context_for] 433 | unless examples[ex[:context_for]] 434 | errors << "Example Context #{ex[:number]} at line #{ex[:line]} references unknown example #{ex[:context_for].inspect}" 435 | $stdout.write "F".colorize(:red) 436 | next 437 | end 438 | 439 | # Either exapand with this external context, or compact using it 440 | case method 441 | when :expand 442 | options[:externalContext] = content 443 | options[:base] = ex[:base] if ex[:base] 444 | args = [StringIO.new(examples[ex[:context_for]][:content]), options] 445 | when :compact, :flatten, nil 446 | options[:base] = ex[:base] if ex[:base] 447 | args = [StringIO.new(examples[ex[:context_for]][:content]), content, options] 448 | end 449 | elsif %w(jsonld html).include?(ex[:ext]) 450 | # Either exapand with this external context, or compact using it 451 | case method 452 | when :expand, :toRdf, :fromRdf 453 | options[:externalContext] = StringIO.new(ex[:context]) if ex[:context] 454 | options[:base] = ex[:base] if ex[:base] 455 | args = [content, options] 456 | when :compact, :flatten 457 | # Fixme how to find context? 458 | options[:base] = ex[:base] if ex[:base] 459 | args = [content, (StringIO.new(ex[:context]) if ex[:context]), options] 460 | end 461 | else 462 | args = [content, options] 463 | end 464 | 465 | if ex[:result_for] 466 | # Source is referenced 467 | # Instead of parsing this example content, parse that which is referenced 468 | unless examples[ex[:result_for]] 469 | errors << "Example #{ex[:number]} at line #{ex[:line]} references unknown example #{ex[:result_for].inspect}" 470 | $stdout.write "F".colorize(:red) 471 | next 472 | end 473 | 474 | # Set argument to referenced content to be parsed 475 | args[0] = if examples[ex[:result_for]][:ext] == 'html' && method == :expand 476 | # If we are expanding, and the reference is HTML, find the first script element. 477 | doc = Nokogiri::HTML.parse(examples[ex[:result_for]][:content]) 478 | 479 | # Get base from document, if present 480 | html_base = doc.at_xpath('/html/head/base/@href') 481 | options[:base] = html_base.to_s if html_base 482 | 483 | script_content = doc.at_xpath(xpath) 484 | unless script_content 485 | errors << "Example #{ex[:number]} at line #{ex[:line]} references example #{ex[:result_for].inspect} with no JSON-LD script element" 486 | $stdout.write "F".colorize(:red) 487 | next 488 | end 489 | StringIO.new(examples[ex[:result_for]][:content]) 490 | elsif examples[ex[:result_for]][:ext] == 'html' && ex[:target] 491 | # Only use the targeted script 492 | doc = Nokogiri::HTML.parse(examples[ex[:result_for]][:content]) 493 | script_content = doc.at_xpath(xpath) 494 | unless script_content 495 | errors << "Example #{ex[:number]} at line #{ex[:line]} references example #{ex[:result_for].inspect} with no JSON-LD script element" 496 | $stdout.write "F".colorize(:red) 497 | next 498 | end 499 | StringIO.new(script_content.to_html) 500 | else 501 | StringIO.new(examples[ex[:result_for]][:content]) 502 | end 503 | 504 | if examples[ex[:result_for]][:content_type] 505 | args[0].define_singleton_method(:content_type) {examples[ex[:result_for]][:content_type]} 506 | end 507 | 508 | # :frame option indicates the frame to use on the referenced content 509 | if ex[:frame] && !examples[ex[:frame]] 510 | errors << "Example #{ex[:number]} at line #{ex[:line]} references unknown frame #{ex[:frame].inspect}" 511 | $stdout.write "F".colorize(:red) 512 | next 513 | elsif ex[:frame] 514 | method = :frame 515 | args = [args[0], StringIO.new(examples[ex[:frame]][:content]), options] 516 | end 517 | 518 | # :context option indicates the context to use on the referenced content 519 | if ex[:context] && !examples[ex[:context]] 520 | errors << "Example #{ex[:number]} at line #{ex[:line]} references unknown context #{ex[:context].inspect}" 521 | $stdout.write "F".colorize(:red) 522 | next 523 | else 524 | case method 525 | when :expand, :toRdf, :fromRdf 526 | options[:externalContext] = StringIO.new(examples[ex[:context]][:content]) if ex[:context] 527 | args = [args[0], options] 528 | when :compact, :flatten 529 | args = [args[0], ex[:context] ? StringIO.new(examples[ex[:context]][:content]) : nil, options] 530 | end 531 | end 532 | end 533 | 534 | # Save example 535 | if example_dir 536 | file_content = content.respond_to?(:rewind) ? (content.rewind; content.read) : content 537 | File.open(File.join(example_dir, ex[:filename]), 'w') {|f| f.write(file_content)} 538 | content.rewind if content.respond_to?(:rewind) 539 | end 540 | 541 | # Save example as YAML 542 | if yaml_dir && ex[:filename].match?(/\.json.*$/) 543 | fn = ex[:filename].sub(/\.json.*$/, '.yaml') 544 | File.open(File.join(yaml_dir, fn), 'w') do |f| 545 | f.puts "Example #{"%03d" % ex[:number]}: #{ex[:title]}" 546 | f.write(::JSON.parse(ex[:content]).to_yaml) 547 | end 548 | end 549 | 550 | # Generate result 551 | # * If result_for is set, this is for the referenced example 552 | # * otherwise, this is for this example 553 | begin 554 | ext = ex[:result_for] ? examples[ex[:result_for]][:ext] : ex[:ext] 555 | result = case method 556 | when nil then nil 557 | when :fromRdf 558 | args[0] = RDF::Reader.for(file_extension: ext).new(args[0], **options) 559 | opts = args.last.is_a?(Hash) ? args.pop : {} 560 | JSON::LD::API.fromRdf(*args, **opts) 561 | when :toRdf 562 | opts = args.last.is_a?(Hash) ? args.pop : {} 563 | RDF::Repository.new << JSON::LD::API.toRdf(*args, **opts) 564 | else 565 | opts = args.last.is_a?(Hash) ? args.pop : {} 566 | JSON::LD::API.method(method).call(*args, **opts) 567 | end 568 | rescue 569 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error generating result: #{$!}" 570 | $stdout.write "F".colorize(:red) 571 | $stderr.puts $!.backtrace.join("\n") if verbose 572 | next 573 | end 574 | 575 | if verbose 576 | if result.is_a?(RDF::Enumerable) 577 | $stderr.puts "result:\n" + result.to_nquads 578 | else 579 | $stderr.puts "result:\n" + result.to_json(JSON::LD::JSON_STATE) 580 | end 581 | end 582 | 583 | begin 584 | if ex[:result_for] 585 | # Compare to expected to result 586 | case ex[:ext] 587 | when 'ttl', 'trig', 'nq', 'html' 588 | reader = RDF::Reader.for(file_extension: ex[:ext]).new(content, **options) 589 | expected = RDF::Repository.new << reader 590 | $stderr.puts "expected:\n" + expected.to_nquads if verbose 591 | when 'table' 592 | expected = begin 593 | table_to_dataset(content.xpath('/html/body/table')) 594 | rescue 595 | errors << "Example #{ex[:number]} at line #{ex[:line]} raised error reading table: #{$!}" 596 | $stderr.puts $!.backtrace.join("\n") if verbose 597 | RDF::Repository.new 598 | end 599 | 600 | if verbose 601 | $stderr.puts "expected:\n" + expected.to_nquads 602 | $stderr.puts "result table:\n" + begin 603 | dataset_to_table(result) 604 | rescue 605 | errors << "Example #{ex[:number]} at line #{ex[:line]} raised error turning into table: #{$!}" 606 | "" 607 | $stderr.puts $!.backtrace.join("\n") if verbose 608 | end 609 | end 610 | else 611 | expected = ::JSON.parse(content.read) 612 | $stderr.puts "expected: " + expected.to_json(JSON::LD::JSON_STATE) if verbose 613 | end 614 | 615 | # Perform appropriate comparsion 616 | if expected.is_a?(RDF::Enumerable) 617 | if !expected.isomorphic_with?(result) 618 | errors << "Example #{ex[:number]} at line #{ex[:line]} not isomorphic with #{examples[ex[:result_for]][:number]}" 619 | $stdout.write "F".colorize(:red) 620 | next 621 | elsif !ex[:no_lint] && !(messages = expected.lint).empty? 622 | # Lint problems in resulting graph. 623 | if verbose 624 | messages.each do |kind, term_messages| 625 | term_messages.each do |term, messages| 626 | $stderr.puts "lint #{kind} #{term}" 627 | messages.each {|m| $stderr.puts " #{m}"} 628 | end 629 | end 630 | end 631 | errors << "Example #{ex[:number]} at line #{ex[:line]} has lint errors" 632 | $stdout.write "F".colorize(:red) 633 | next 634 | end 635 | else 636 | unless result == expected 637 | errors << "Example #{ex[:number]} at line #{ex[:line]} not equivalent to #{examples[ex[:result_for]][:number]}" 638 | $stdout.write "F".colorize(:red) 639 | next 640 | end 641 | end 642 | end 643 | rescue 644 | errors << "Example #{ex[:number]} at line #{ex[:line]} parse error comparing result: #{$!}" 645 | $stdout.write "F".colorize(:red) 646 | $stderr.puts $!.backtrace.join("\n") if verbose 647 | next 648 | end 649 | 650 | if ex[:warn] 651 | warnings << ex[:warn] 652 | $stdout.write "w".colorize(:yellow) 653 | else 654 | $stdout.write ".".colorize(:green) 655 | end 656 | end 657 | 658 | $stdout.puts "\nWarnings:" unless warnings.empty? 659 | warnings.each {|e| $stdout.puts " #{e}".colorize(:yellow)} 660 | $stdout.puts "\nErrors:" unless errors.empty? 661 | errors.each {|e| $stdout.puts " #{e}".colorize(:red)} 662 | num_errors += errors.length 663 | end 664 | 665 | if num_errors == 0 666 | $stdout.puts "\nok".colorize(:green) 667 | else 668 | exit(1) 669 | end 670 | 671 | exit(0) 672 | -------------------------------------------------------------------------------- /common/jsonld.js: -------------------------------------------------------------------------------- 1 | const jsonld = { 2 | // Add as the respecConfig localBiblio variable 3 | // Extend or override global respec references 4 | localBiblio: { 5 | // aliases to known references 6 | "swagger": { 7 | "publisher": "Open API Initiative (OAI)", 8 | "title": "SWAGGER: The World's Most Popular Framework for APIs", 9 | "href" : "http://swagger.io", 10 | "date": "2016-01-01" 11 | }, 12 | "json-ld-best-practice-caching": { 13 | "authors": [ 14 | "Manu Sporny", 15 | "David Longley" 16 | ], 17 | "title": "JSON-LD Best Practice: Context Caching", 18 | "href": "http://manu.sporny.org/2016/json-ld-context-caching/", 19 | "date": "2016-04-24" 20 | }, 21 | "seo-strings-to-things": { 22 | "authors": ["Aaron Bradley"], 23 | "title": "Semantic SEO: Making the Shift from Strings to Things", 24 | "href": "http://www.seoskeptic.com/semantic-seo-making-shift-strings-things/", 25 | "date": "2013-10-02", 26 | "publisher": "SEO Skeptic" 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSON-LD Best Practices 5 | 6 | 7 | 8 | 9 | 55 | 59 | 60 | 61 |
62 |

63 | Developers share a common problem: they want a simple, 64 | but extensible way to create an API for a web service that gets the job done, 65 | doesn't design them into a corner, 66 | and allows developers to easily interact with their service without reinventing the wheel. 67 | JSON-LD [[JSON-LD]] has become an important solution, 68 | as it bridges the gap between formally data 69 | and more colloquial JSON interfaces used in APIs from numerous providers. 70 | This guide attempts to define certain best practices for publishing data using JSON-LD, 71 | and interacting with such services. 72 |

73 |
74 | 75 |
76 |

This unofficial document has been developed by the 77 | JSON-LD Working Group.

78 |
79 | 80 |
81 |

This document describes best practices for generating JSON-LD. 82 | Where normative language is used, it should be considered advisory. 83 |

84 |
85 | 86 |
87 |

Introduction

88 |

89 | Coming up with a data format for your API is a common problem. 90 | It can be hard to choose between different data representations, 91 | what names you want to pick, 92 | and even harder if you want to leave room for extensibility. 93 | How do you make all these decisions? 94 | How do you make your API easy to use so people can use short strings to reference common things, 95 | but URLs to enable people to come up with their own so it isn't limiting? 96 | How can you make it easy for other people to add their own data in and make it interoperable? 97 | How do you consume data from other similar apps? 98 | There are technologies that can help you do this. 99 | Now, it isn't perfect – sometimes it won't solve your problem, 100 | but it could maybe solve a lot of them. 101 |

102 |

103 | The use of JSON on the web has grown immensely in the last decade, 104 | particularly with the explosion of APIs that eschew XML 105 | in favor of what is considered to be a more developer friendly format 106 | which is directly compatible with JavaScript. 107 | As a result, different sites have chosen their own 108 | proprietary representations for interacting with their sites, 109 | sometimes described using frameworks such as [[swagger]] 110 | which imply a particular URI composition for interacting with their services. 111 | This practice leads to vendor-specific semantic silos, 112 | where the meaning of a particular JSON document makes sense 113 | only by programming directly to the API documentation for a given service. 114 |

115 |

116 | show examples from GitHub, Twitter, …? 117 |

118 |

119 | As services grow they often introduce incompatible changes leading 120 | to a Version 2 or Version 3 of their API 121 | requiring developers to update client code 122 | to properly handle JSON documents. 123 | In many cases, even small changes can lead to incompatibilities. 124 | Additionally, composing information from multiple APIs becomes problematic, 125 | due to namespace or document format conventions that may differ between API endpoints. 126 | Moreover, the same principles are often repeated across different endpoints 127 | using arbitrary identifiers (name, email, website, etc.); 128 | the community needs to learn to stop repeating itself 129 | (DRY concept) 130 | and reuse common conventions, 131 | although this does not necessarily have to mean using exactly 132 | the same identifiers within the JSON itself (see JSON-LD Context). 133 |

134 |

135 | This Note proposes to outline a number of best practices 136 | for API designers or JSON developers 137 | based on the principles of separation of data model from syntax, 138 | the use of discoverable identifiers describing document contents, 139 | and general organizing principles that allow documents 140 | to be machine understandable 141 | (read, interpreted as JSON-LD using Linked Data, 142 | RDF and RDFS vocabulary, and data model principles). 143 |

144 |

145 | Key among these is the notion of vocabulary re-use, 146 | so that each endpoint does not need to separately describe 147 | the properties and structure of their JSON documents. 148 | Schema.org provides a great example of doing this, 149 | and includes an extension mechanism 150 | that may already be familiar to API designers. 151 |

152 |

153 | JSON-LD is JSON, 154 | and good JSON-LD is first and foremost good JSON. 155 | Since it is also Linked Data, 156 | developers and especially data publishers may find 157 | further useful advice at Data on the Web Best Practices [[dwbp]] 158 | and Best Practices for Publishing Linked Data [[ld-bp]]. 159 |

160 |
161 | 162 |
163 | 164 |
165 |

Terminology

166 |
167 |
JSON-LD
168 |
169 | JSON-LD [[JSON-LD]] is a lightweight Linked Data format. 170 | It is easy for humans to read and write. 171 | It is based on the already successful JSON format 172 | and provides a way to help JSON data interoperate at Web-scale. 173 | JSON-LD is an ideal data format for programming environments, 174 | REST Web services, and unstructured databases 175 | such as CouchDB and MongoDB. 176 |
177 |
JSON-LD Context
178 |
179 | In JSON-LD, 180 | a context 181 | is used to map terms, 182 | i.e., properties with associated values in an JSON document, to URLs. 183 | A term is a short word 184 | that expands to a URL. 185 | Terms may be generally defined as any valid JSON string 186 | other than a JSON-LD keyword. 187 |
188 |
Linked Data
189 |
190 | Linked Data [[linked-data]] is a way to create 191 | a network of standards-based machine interpretable data 192 | across different documents and Web sites. 193 | It allows an application to start at one piece of Linked Data, 194 | and follow embedded links to other pieces of Linked Data 195 | that are hosted on different sites across the Web. 196 |
197 |
198 |
199 | 200 |
201 |

Data Documents

202 | 203 |
204 |

205 | Publish data using developer friendly JSON 206 | JSON [[json]] is the most popular format for publishing data through APIs; 207 | developers like it, 208 | it is easy to parse, 209 | and it is supported natively in most programming languages. 210 |

211 |
212 |

For example, the following is reasonably idiomatic JSON which can also be interpreted as JSON-LD, given the appropriate context.

213 |
216 |     {
217 |       "name": "Barack Obama",
218 |       "givenName": "Barack",
219 |       "familyName": "Obama",
220 |       "jobTitle": "44th President of the United States"
221 |     }
222 |   
223 | 224 |
225 |

226 | Use a top-level object 227 | JSON documents may be in the form of a object, 228 | or an array of objects. 229 | For most purposes, developers need a single entry point, 230 | so the JSON SHOULD be in the form of a single top-level object. 231 |

232 |
233 | 234 |
235 |

236 | Use native values 237 | When possible, property values SHOULD use native JSON datatypes 238 | such as numbers (integer, decimal and floating point) 239 | and booleans (`true` and `false`). 240 |

241 |
242 |

JSON has a single numeric type, 243 | so using native representation of numbers can lose precision. 244 |

245 | 246 |
247 |

248 | Assume arrays are unordered 249 | JSON specifies that the values in an array are ordered, 250 | however in many cases arrays are also used for values which are unordered. 251 | Unless specified within the JSON-LD Context, 252 | multiple array values SHOULD be presumed to be unordered. (See Lists and Sets in [[JSON-LD]]).

253 |
254 | 255 |
256 |

257 | Use well-known identifiers when describing data 258 | By sticking to basic JSON data expression, 259 | and providing a JSON-LD Context, 260 | all keys used within a JSON document can have unambiguous meaning, 261 | as they bind to URLs which describe their meaning. 262 |

263 |
264 |

By adding an `@context` entry, 265 | the previous example can now be interpreted as JSON-LD.

266 |
269 |     {
270 |       ****"@context": "http://schema.org"****,
271 |       "name": "Barack Obama",
272 |       "givenName": "Barack",
273 |       "familyName": "Obama",
274 |       "jobTitle": "44th President of the United States"
275 |     }
276 |   
277 |
278 |

When expanding such a data representation, 279 | a JSON-LD processor replaces these terms with the URIs they expand to 280 | (as well as making property values unambiguous): 281 |

282 |
285 |       [
286 |         {
287 |           "http://schema.org/familyName": [{"@value": "Obama"}],
288 |           "http://schema.org/givenName": [{"@value": "Barack"}],
289 |           "http://schema.org/jobTitle": [{"@value": "44th President of the United States"}],
290 |           "http://schema.org/name": [{"@value": "Barack Obama"}]
291 |         }
292 |       ]
293 |     
294 |

Expanded form is not useful as is, 295 | but is necessary for performing further algorithmic transformations 296 | of JSON-LD data and is useful when validating 297 | that JSON-LD entity descriptions say what the publisher means. 298 |

299 |
300 | 301 |
302 |

303 | Provide one or more types for JSON objects 304 | Principles of Linked Data dictate that 305 | messages SHOULD be self describing, 306 | which includes adding a `type` to such messages.

307 |
308 |

Many APIs use JSON messages 309 | where the type of information being conveyed is inferred from the retrieval endpoint. 310 | For example, when retrieving information about a Github Commit, 311 | you might see the following response:

312 |
315 |     {
316 |       "sha": "7638417db6d59f3c431d3e1f261cc637155684cd",
317 |       "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd",
318 |       "author": {
319 |         "date": "2014-11-07T22:01:45Z",
320 |         "name": "Scott Chacon",
321 |         "email": "schacon@gmail.com"
322 |       },
323 |       "committer": {
324 |         "date": "2014-11-07T22:01:45Z",
325 |         "name": "Scott Chacon",
326 |         "email": "schacon@gmail.com"
327 |       },
328 |       "message": "added readme, because im a good github citizen\n",
329 |       "tree": {
330 |         "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/691272480426f78a0138979dd3ce63b77f706feb",
331 |         "sha": "691272480426f78a0138979dd3ce63b77f706feb"
332 |       },
333 |       "parents": [
334 |         {
335 |           "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5",
336 |           "sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5"
337 |         }
338 |       ]
339 |     }
340 |   
341 |

The only way to know this is a commit 342 | s to infer it based on the published API documentation, 343 | and the fact that it was returned from an endpoint 344 | defined for retrieving information about commits.

345 |
348 |     {
349 |       "@context": "http://schema.org",
350 |       "id": "http://www.wikidata.org/entity/Q76",
351 |       ****"type": "Person"****,
352 |       "name": "Barack Obama",
353 |       "givenName": "Barack",
354 |       "familyName": "Obama",
355 |       "jobTitle": "44th President of the United States"
356 |     }
357 |   
358 | 359 |
360 |

361 | Identify objects with a unique identifier 362 | Entities described in JSON objects often describe web resources having a URL; 363 | entity descriptions SHOULD use an identifier uniquely identifying that entity. 364 | In this case, using the resource location as the identity of the object 365 | is consistent with this practice.

366 |
367 |

Adding an `id` entry (an alias for `@id`) allows the same person 368 | to be referred to from different locations.

369 |
372 |     {
373 |       "@context": "http://schema.org",
374 |       ****"id": "http://www.wikidata.org/entity/Q76"****,
375 |       "type": "Person",
376 |       "name": "Barack Obama",
377 |       "givenName": "Barack",
378 |       "familyName": "Obama",
379 |       "jobTitle": "44th President of the United States"
380 |     }
381 |   
382 |

There can be ambiguity if an identifier describes the entity description, 383 | or directly represents that entity itself. 384 | As an example, Barack Obama may have a Wikidata entry `http://www.wikidata.org/entity/Q76`, 385 | but it would be a mistake to say that `http://www.wikidata.org/entity/Q76` is Barack Obama. 386 | However, it is common to use this pattern, 387 | particularly if the type of the entity describes a Person, 388 | rather than a WebPage.

389 | 390 |
391 |

392 | Things not strings 393 | When describing attributes, 394 | entity references SHOULD be used instead of string literals.

395 |
396 |

In some cases, when describing an attribute of an entity, 397 | it is tempting to using string values which have no independent meaning. 398 | Such values are often used for well known things. 399 | A JSON-LD context can define a term for such values, 400 | which allow them to appear as strings within the message, 401 | but be associated with specific identifiers. 402 | In this case, the property must be defined with type `@vocab` 403 | so that values will be interpreted relative to a vocabulary 404 | rather than the file location.

405 |
408 |     {
409 |       "@context": ["http://schema.org", ****{
410 |         "gender": {"@id": "schema:gender", "@type": "@vocab"}
411 |       }****],
412 |       "id": "http://www.wikidata.org/entity/Q76",
413 |       "type": "Person",
414 |       "name": "Barack Obama",
415 |       "givenName": "Barack",
416 |       "familyName": "Obama",
417 |       "jobTitle": "44th President of the United States",
418 |       ****"gender": "Male"****
419 |     }
420 |   
421 | 422 |
423 |

424 | Nest referenced inline objects 425 | When multiple related entity descriptions are provided inline, 426 | related entities SHOULD be nested.

427 |
428 |

For example, when relating one entity to another, 429 | where the related entity is described in the same message:

430 |
433 |     {
434 |       "@context": "http://schema.org",
435 |       "id": "http://www.wikidata.org/entity/Q76",
436 |       "type": "Person",
437 |       "name": "Barack Obama",
438 |       "givenName": "Barack",
439 |       "familyName": "Obama",
440 |       "jobTitle": "44th President of the United States",
441 |       ****"spouse": {
442 |         "id": "http://www.wikidata.org/entity/Q13133",
443 |         "type": "Person",
444 |         "name": "Michelle Obama",
445 |         "spouse": "http://www.wikidata.org/entity/Q76"
446 |       }****
447 |     }
448 |   
449 |

In this example, the `spouse` relationship is bi-directional, 450 | we have arbitrarily rooted the message with Barack Obama, 451 | and created a symmetric relationship 452 | from Michelle back to Barack by reference, 453 | rather than by nesting.

454 | 455 |
456 |

457 | When describing an inverse relationship, use a referenced property 458 | FIXME 459 |

460 |
461 |
462 | 463 |
464 |

Contexts

465 | 466 |
467 |

468 | External references SHOULD use typed term 469 | When using a property intended to reference another entity, 470 | properties SHOULD be defined to type string values as being references.

471 |
472 |

For example, the `schema:image` property a `Thing` to an `Image`:

473 |
476 |     {
477 |       "@context": "http://schema.org",
478 |       "id": "http://www.wikidata.org/entity/Q76",
479 |       "type": "Person",
480 |       "name": "Barack Obama",
481 |       "givenName": "Barack",
482 |       "familyName": "Obama",
483 |       "jobTitle": "44th President of the United States",
484 |       ****"image": "https://commons.wikimedia.org/wiki/File:President_Barack_Obama.jpg"****
485 |     }
486 |   
487 |

This will be interpreted as a reference, 488 | rather than a string literal, 489 | because (at the time of publication), 490 | the schema.org JSON-LD Context defines `image` to be of type `@id`:

491 |
494 |     {
495 |       "@context": {
496 |         ####...####
497 |         "image": { "@id": "schema:image", ****"@type": "@id"****}####,
498 |         ...####
499 |       }
500 |     }
501 |   
502 |

If not defined as such in a remote context, 503 | terms may be (re-) defined in a local context:

504 |
507 |     {
508 |       "@context": ["http://schema.org", ****{
509 |         "image": { "@id": "schema:image", "@type": "@id"}
510 |       }****],
511 |       "id": "http://www.wikidata.org/entity/Q76",
512 |       "type": "Person",
513 |       "name": "Barack Obama",
514 |       "givenName": "Barack",
515 |       "familyName": "Obama",
516 |       "jobTitle": "44th President of the United States",
517 |       "image": "https://commons.wikimedia.org/wiki/File:President_Barack_Obama.jpg"
518 |     }
519 |   
520 | 521 |
522 |

523 | Ordering of array elements 524 | Unless specifically described ordered as an `@list`, 525 | do not depend on the order of elements in an array.

526 |
527 |

By default, 528 | 529 | arrays in JSON-LD do not convey any ordering of contained elements 530 | . 531 | However, for the processing of contexts, 532 | the ordering of elements in arrays does matter. 533 | When writing array-based contexts, this fact should be kept in mind.

534 |

Ordered contexts in arrays allow inheritance 535 | and overriding of context entries. 536 | When processing the following example, 537 | the first `name` entry will be overridden by the second `name` entry.

538 |
541 |     {
542 |       "@context": [
543 |         {
544 |           "id": "@id",
545 |           "name": "http://schema.org/name"
546 |         },
547 |         {
548 |           "name": "http://xmlns.com/foaf/0.1/name"
549 |         }
550 |       ],
551 |       "@id": "http://www.wikidata.org/entity/Q76",
552 |       ****"name": "Barack Obama"****
553 |     }
554 |   
555 |

Order is important when processing 556 | protected terms. 557 | While the first example will cause a term redefinition error, 558 | the second example will not throw this error.

559 |
562 |     {
563 |       "@context": [
564 |         {
565 |           "@version": 1.1,
566 |           "name": {
567 |             "@id": "http://schema.org/name",
568 |             "@protected": true
569 |           }
570 |         },
571 |         {
572 |           "name": "http://xmlns.com/foaf/0.1/name"
573 |         }
574 |       ],
575 |       "@id": "http://www.wikidata.org/entity/Q76",
576 |       ****"name": "Barack Obama"****
577 |     }
578 |   
579 |
582 |     {
583 |       "@context": [
584 |         {
585 |           "name": "http://xmlns.com/foaf/0.1/name"
586 |         },
587 |         {
588 |           "@version": 1.1,
589 |           "Person": "http://schema.org/Person",
590 |           "knows": "http://schema.org/knows",
591 |           "name": {
592 |             "@id": "http://schema.org/name",
593 |             "@protected": true
594 |           }
595 |         }
596 |       ],
597 |       "@id": "http://www.wikidata.org/entity/Q76",
598 |       ****"name": "Barack Obama"****
599 |     }
600 |   
601 |
602 | 603 |
604 |

Publishing

605 | 606 |
607 |

608 | Provide a representation of the entity related by URL 609 | When dereferencing an entity related via a URL, 610 | the location SHOULD provide a representation of that entity.

611 |
612 |

This practices replicates that described in [[ld-bp]] 613 | 614 | Provide at least one machine-readable representation 615 | of the resource identified by the URI 616 | .

617 |

Corollaries to this best practice is that 618 | Cool URIs don't change [[cooluris]], 619 | meaning that URLs describing entities SHOULD be stable 620 | and not depend on variable information. 621 | Also, the URL used to identify an entity is the best API endpoint of that entity 622 | (see also ).

623 |
624 | 625 |
626 |

Consuming

627 | 628 |

While most use of JSON-LD SHOULD NOT require a client 629 | to change the data representation, 630 | JSON-LD does allow the use of various algorithms 631 | to re-shape a JSON-LD document. 632 | These require the use of the JSON-LD Context, 633 | which is typically represented using a link to a remote document. 634 | Because it is remote, 635 | processing time can be severely impacted 636 | by the time it takes to retrieve this context.

637 |
638 |

639 | Cache JSON-LD Contexts 640 | Services providing a JSON-LD Context SHOULD 641 | set HTTP cache-control headers to allow liberal caching of such contexts, 642 | and clients SHOULD attempt to use a locally cached version 643 | of these documents.

644 |
645 |

Typically, libraries used to process JSON-LD documents 646 | should do this for you. 647 | (See also [[json-ld-best-practice-caching]]).

648 |
649 | 650 |
651 |

Serializing Large Collections

652 |

Describe schema.org extension using Role sub-class, 653 | Hydra collections, and LDP collections.

654 |
655 | 656 |
657 |

Reuse Vocabularies

658 |

Focus on schema.org?

659 |
660 | 661 |
662 |

Describe API affordances

663 |

Describe the use of schema.org Actions and work in Hydra.

664 |

Describe anti-pattern of URI construction emphasizing affordances.

665 |
666 | 667 |
668 |

API Versioning

669 |

Remember that Cool URIs don't change [cooluris]; 670 | correctly modeling data allows changes data representation to be limited.

671 |

Describe the use of API keys for controlling API versions, 672 | rather than the use of different versioned URLs.

673 |
674 | 675 | 676 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": ["wg/json-ld"], 3 | "contacts": ["plehegar"], 4 | "repo-type": "note", 5 | "short-name": "json-ld11-bp" 6 | } 7 | --------------------------------------------------------------------------------