From dd350b2e3466cabe0e8bce6e60253d0e58d5cada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Annama=CC=81ria=20Koren?= Date: Fri, 6 Nov 2026 10:00:10 +0100 Subject: [PATCH 1/2] add training section with slide decks and explanations, CMEM-8382 Draft of a Training section that presents the workshop material as reveal.js slides above the fold and a written explanation per slide below. - add modules RDF and Linked Data Foundations and SPARQL Basics, plus an overview of the two-day training - vendor reveal.js 5.2.1 under assets/revealjs with an eccenca theme, no CDN - sync deck and explanations: current slide links to its section, headings show their slide - allow iframe in rumdl, skip verbatim docs/ HTML in the social-card check --- .markdownlint.jsonc | 1 + docs/.pages | 1 + docs/assets/revealjs/LICENSE | 19 + docs/assets/revealjs/eccenca-theme.css | 303 +++++++++ docs/assets/revealjs/plugin/markdown.js | 7 + docs/assets/revealjs/plugin/notes.js | 1 + docs/assets/revealjs/reset.css | 30 + docs/assets/revealjs/reveal.css | 9 + docs/assets/revealjs/reveal.js | 8 + docs/assets/revealjs/training-deck.js | 99 +++ docs/assets/training.css | 116 ++++ docs/assets/training.js | 152 +++++ docs/training/.pages | 5 + docs/training/index.md | 67 ++ docs/training/rdf-and-linked-data/index.md | 328 ++++++++++ docs/training/rdf-and-linked-data/slides.html | 575 ++++++++++++++++++ docs/training/sparql-basics/index.md | 338 ++++++++++ docs/training/sparql-basics/slides.html | 494 +++++++++++++++ mkdocs.yml | 2 + nav.yml | 6 + tools/check_zensical_output.py | 5 + 21 files changed, 2566 insertions(+) create mode 100644 docs/assets/revealjs/LICENSE create mode 100644 docs/assets/revealjs/eccenca-theme.css create mode 100644 docs/assets/revealjs/plugin/markdown.js create mode 100644 docs/assets/revealjs/plugin/notes.js create mode 100644 docs/assets/revealjs/reset.css create mode 100644 docs/assets/revealjs/reveal.css create mode 100644 docs/assets/revealjs/reveal.js create mode 100644 docs/assets/revealjs/training-deck.js create mode 100644 docs/assets/training.css create mode 100644 docs/assets/training.js create mode 100644 docs/training/.pages create mode 100644 docs/training/index.md create mode 100644 docs/training/rdf-and-linked-data/index.md create mode 100644 docs/training/rdf-and-linked-data/slides.html create mode 100644 docs/training/sparql-basics/index.md create mode 100644 docs/training/sparql-basics/slides.html diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc index 138595d1e..f06745339 100644 --- a/.markdownlint.jsonc +++ b/.markdownlint.jsonc @@ -20,6 +20,7 @@ "div", "figcaption", "figure", + "iframe", "img", "key", "name", diff --git a/docs/.pages b/docs/.pages index 41abd5958..61a95a63e 100644 --- a/docs/.pages +++ b/docs/.pages @@ -9,4 +9,5 @@ nav: - Develop: develop - Release Notes: release-notes - Tutorials: tutorials + - Training: training diff --git a/docs/assets/revealjs/LICENSE b/docs/assets/revealjs/LICENSE new file mode 100644 index 000000000..38ff0eb42 --- /dev/null +++ b/docs/assets/revealjs/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2011-2024 Hakim El Hattab, http://hakim.se, and reveal.js contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/docs/assets/revealjs/eccenca-theme.css b/docs/assets/revealjs/eccenca-theme.css new file mode 100644 index 000000000..9a7d7d796 --- /dev/null +++ b/docs/assets/revealjs/eccenca-theme.css @@ -0,0 +1,303 @@ +/* + * eccenca theme for the training decks. + * + * reveal.js 5.2.1 (MIT, see LICENSE in this directory) is vendored here, + * not loaded from a CDN, for the same reason the web fonts are self-hosted: + * no visitor IP address may reach a third-party origin. The bundled reveal.js + * themes are not used because they @import their own font files. This theme + * reuses the site's self-hosted Roboto from assets/fonts.css instead. + * + * Vendored files: reveal.js, reveal.css, reset.css, plugin/markdown.js, + * plugin/notes.js (all from the reveal.js@5.2.1 npm package, source map + * comment removed). + */ + +:root { + --ecc-orange: #f39200; + --ecc-orange-dark: #c97800; + --ecc-slate: #2e3a44; + --ecc-slate-light: #4a5864; + --ecc-text: #2e3a44; + --ecc-muted: #6b7782; + --ecc-bg: #f4f5f7; + --ecc-code-bg: #e9e4ef; + --ecc-callout: #ffc000; + --ecc-blue: #0f5e98; + + --r-background-color: var(--ecc-bg); + --r-main-font: "Roboto", Helvetica, Arial, sans-serif; + --r-main-font-size: 24px; + --r-main-color: var(--ecc-text); + --r-block-margin: 14px; + --r-heading-margin: 0 0 22px 0; + --r-heading-font: "Roboto", Helvetica, Arial, sans-serif; + --r-heading-color: var(--ecc-slate); + --r-heading-line-height: 1.15; + --r-heading-font-weight: 300; + --r-heading1-size: 2em; + --r-heading2-size: 1.45em; + --r-heading3-size: 1.15em; + --r-code-font: "Roboto Mono", monospace; + --r-link-color: var(--ecc-blue); + --r-link-color-hover: var(--ecc-orange-dark); + --r-selection-background-color: var(--ecc-orange); + --r-selection-color: #fff; +} + +.reveal-viewport { + background: var(--r-background-color); +} + +.reveal { + font-family: var(--r-main-font); + font-size: var(--r-main-font-size); + font-weight: 400; + color: var(--r-main-color); +} + +.reveal .slides { + text-align: left; +} + +.reveal .slides section { + height: 100%; + padding: 30px 46px 52px; + box-sizing: border-box; +} + +/* eccenca logo mark and copyright line in every slide's footer */ +.reveal .slides section::after { + content: "© eccenca GmbH"; + position: absolute; + left: 0; + right: 0; + bottom: 16px; + text-align: center; + font-size: 11px; + color: var(--ecc-muted); +} + +.reveal h1, +.reveal h2, +.reveal h3, +.reveal h4 { + margin: var(--r-heading-margin); + color: var(--r-heading-color); + font-family: var(--r-heading-font); + font-weight: var(--r-heading-font-weight); + line-height: var(--r-heading-line-height); + text-transform: none; +} + +.reveal h1 { font-size: var(--r-heading1-size); } +.reveal h2 { font-size: var(--r-heading2-size); } +.reveal h3 { font-size: var(--r-heading3-size); font-weight: 500; } + +.reveal p { + margin: var(--r-block-margin) 0; + line-height: 1.35; +} + +.reveal strong { font-weight: 600; } + +.reveal ul, +.reveal ol { + display: block; + margin: 0 0 0 1.1em; +} + +.reveal li { + margin: 0.25em 0; + line-height: 1.3; +} + +.reveal li li { + font-size: 0.85em; +} + +.reveal a { + color: var(--r-link-color); + text-decoration: underline; +} + +.reveal a:hover { + color: var(--r-link-color-hover); +} + +.reveal code { + font-family: var(--r-code-font); + font-size: 0.85em; +} + +.reveal pre { + display: block; + width: 100%; + margin: var(--r-block-margin) 0; + font-size: 0.7em; + line-height: 1.4; + box-shadow: none; +} + +.reveal pre code { + display: block; + padding: 10px 16px; + overflow: auto; + max-height: 380px; + background: var(--ecc-code-bg); + color: var(--ecc-text); + white-space: pre; + word-wrap: normal; +} + +.reveal pre.data code { background: #dfe8f3; } +.reveal pre.result code { background: #e8eedd; } + +.reveal table { + margin: 10px 0; + border-collapse: collapse; + font-size: 0.75em; +} + +.reveal table th, +.reveal table td { + padding: 6px 14px; + border: 1px solid var(--ecc-slate); + text-align: left; +} + +.reveal table th { background: #e7e9ec; font-weight: 600; } +.reveal table td { background: #fff; } + +/* the yellow "what the query asks" bubble of the original decks */ +.reveal .ask { + display: inline-block; + padding: 10px 18px; + border-radius: 8px; + background: var(--ecc-callout); + font-size: 0.6em; + font-style: italic; +} + +.reveal .label { + display: inline-block; + margin: 0 0 6px; + font-size: 0.5em; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--ecc-muted); + text-transform: uppercase; +} + +.reveal .small { font-size: 0.7em; } +.reveal .muted { color: var(--ecc-muted); } + +.reveal .columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + align-items: start; +} + +.reveal .columns > * { min-width: 0; } + +.reveal .pill { + display: inline-block; + padding: 4px 18px; + border-radius: 999px; + background: var(--ecc-orange); + color: #fff; + font-size: 0.5em; + font-weight: 600; +} + +/* title and section divider slides */ +.reveal .slides section.title-slide, +.reveal .slides section.divider { + display: flex !important; + flex-direction: column; + justify-content: flex-end; + padding-bottom: 66px; +} + +/* the backgrounds themselves are set with data-background-* on the section */ + +.reveal .slides section.title-slide h1, +.reveal .slides section.title-slide h2, +.reveal .slides section.title-slide p, +.reveal .slides section.divider h1, +.reveal .slides section.divider h2, +.reveal .slides section.divider p, +.reveal .slides section.title-slide::after, +.reveal .slides section.divider::after { + color: #fff; +} + +.reveal .slides section.title-slide h1, +.reveal .slides section.divider h1 { + font-weight: 400; + text-transform: uppercase; + letter-spacing: 0.01em; +} + +.reveal .slides section.title-slide .brand, +.reveal .slides section.divider .brand { + position: absolute; + top: 44px; + left: 46px; + font-size: 1.4em; + font-weight: 700; + color: #fff; + letter-spacing: -0.02em; +} + +.reveal .slides section.title-slide .brand small, +.reveal .slides section.divider .brand small { + display: block; + font-size: 0.4em; + font-weight: 300; + letter-spacing: 0.02em; +} + +/* inline SVG diagrams */ +.reveal svg.diagram { + display: block; + max-width: 100%; + height: auto; + margin: 10px auto; + font-family: var(--r-main-font); +} + +.reveal svg.diagram .node { fill: #dfeac6; stroke: #8fae4f; } +.reveal svg.diagram .lit { fill: #f8cfb3; stroke: #d98a5a; } +.reveal svg.diagram .edge { stroke: #3c6fb0; stroke-width: 2; fill: none; } +.reveal svg.diagram text { fill: var(--ecc-text); } + +/* Semantic Web layer cake */ +.reveal .stack { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 6px; + width: 600px; + margin: 0 auto; + font-size: 0.5em; + text-align: center; +} + +.reveal .stack div { + padding: 10px 4px; + border: 1px solid #9aa3ab; + background: #e2e4e7; +} + +/* progress bar and controls in eccenca orange */ +.reveal .progress { color: var(--ecc-orange); height: 4px; } +.reveal .controls { color: var(--ecc-orange); } +.reveal .slide-number a { text-decoration: none; color: inherit; } +.reveal .slide-number { + background: transparent; + color: var(--ecc-muted); + font-size: 11px; + left: 46px; + right: auto; + bottom: 14px; +} diff --git a/docs/assets/revealjs/plugin/markdown.js b/docs/assets/revealjs/plugin/markdown.js new file mode 100644 index 000000000..2511e4109 --- /dev/null +++ b/docs/assets/revealjs/plugin/markdown.js @@ -0,0 +1,7 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealMarkdown=t()}(this,(function(){"use strict";function e(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}let t={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const n=/[&<>"']/,s=new RegExp(n.source,"g"),r=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,i=new RegExp(r.source,"g"),l={"&":"&","<":"<",">":">",'"':""","'":"'"},o=e=>l[e];function a(e,t){if(t){if(n.test(e))return e.replace(s,o)}else if(r.test(e))return e.replace(i,o);return e}const c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function h(e){return e.replace(c,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const p=/(^|[^\[])\^/g;function u(e,t){e="string"==typeof e?e:e.source,t=t||"";const n={replace:(t,s)=>(s=(s=s.source||s).replace(p,"$1"),e=e.replace(t,s),n),getRegex:()=>new RegExp(e,t)};return n}const g=/[^\w:]/g,d=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function f(e,t,n){if(e){let e;try{e=decodeURIComponent(h(n)).replace(g,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!d.test(n)&&(n=function(e,t){k[" "+e]||(x.test(e)?k[" "+e]=e+"/":k[" "+e]=_(e,"/",!0));e=k[" "+e];const n=-1===e.indexOf(":");return"//"===t.substring(0,2)?n?t:e.replace(m,"$1")+t:"/"===t.charAt(0)?n?t:e.replace(b,"$1")+t:e+t}(t,n));try{n=encodeURI(n).replace(/%25/g,"%")}catch(e){return null}return n}const k={},x=/^[^:]+:\/*[^/]*$/,m=/^([^:]+:)[\s\S]*$/,b=/^([^:]+:\/*[^/]*)[\s\S]*$/;const w={exec:function(){}};function y(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),n.length>t)n.splice(t);else for(;n.length1;)1&t&&(n+=e),t>>=1,e+=e;return n+e}function z(e,t,n,s){const r=t.href,i=t.title?a(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:a(l)}}class S{constructor(e){this.options=e||t}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:_(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=_(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n,s,r,i,l,o,a,c,h,p,u,g,d=t[1].trim();const f=d.length>1,k={type:"list",raw:"",ordered:f,start:f?+d.slice(0,-1):"",loose:!1,items:[]};d=f?`\\d{1,9}\\${d.slice(-1)}`:`\\${d}`,this.options.pedantic&&(d=f?d:"[*+-]");const x=new RegExp(`^( {0,3}${d})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(g=!1,t=x.exec(e))&&!this.rules.block.hr.test(e);){if(n=t[0],e=e.substring(n.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),h=e.split("\n",1)[0],this.options.pedantic?(i=2,u=c.trimLeft()):(i=t[2].search(/[^ ]/),i=i>4?1:i,u=c.slice(i),i+=t[1].length),o=!1,!c&&/^ *$/.test(h)&&(n+=h+"\n",e=e.substring(h.length+1),g=!0),!g){const t=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),s=new RegExp(`^ {0,${Math.min(3,i-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:\`\`\`|~~~)`),l=new RegExp(`^ {0,${Math.min(3,i-1)}}#`);for(;e&&(p=e.split("\n",1)[0],h=p,this.options.pedantic&&(h=h.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!r.test(h))&&!l.test(h)&&!t.test(h)&&!s.test(e);){if(h.search(/[^ ]/)>=i||!h.trim())u+="\n"+h.slice(i);else{if(o)break;if(c.search(/[^ ]/)>=4)break;if(r.test(c))break;if(l.test(c))break;if(s.test(c))break;u+="\n"+h}o||h.trim()||(o=!0),n+=p+"\n",e=e.substring(p.length+1),c=h.slice(i)}}k.loose||(a?k.loose=!0:/\n *\n *$/.test(n)&&(a=!0)),this.options.gfm&&(s=/^\[[ xX]\] /.exec(u),s&&(r="[ ] "!==s[0],u=u.replace(/^\[[ xX]\] +/,""))),k.items.push({type:"list_item",raw:n,task:!!s,checked:r,loose:!1,text:u}),k.raw+=n}k.items[k.items.length-1].raw=n.trimRight(),k.items[k.items.length-1].text=u.trimRight(),k.raw=k.raw.trimRight();const m=k.items.length;for(l=0;l"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));k.loose=t}if(k.loose)for(l=0;l$/,"$1").replace(this.rules.inline._escapes,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:y(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let n,s,r,i,l=e.align.length;for(n=0;n({text:e})));for(l=e.header.length,s=0;s/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):a(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=_(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const n=e.length;let s=0,r=0;for(;r-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),z(t,{href:n?n.replace(this.rules.inline._escapes,"$1"):n,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let e=(n[2]||n[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return z(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;const r=s[1]||s[2]||"";if(!r||r&&(""===n||this.rules.inline.punctuation.exec(n))){const n=s[0].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=r.length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=e.slice(0,n+s.index+(s[0].length-r.length)+i);if(Math.min(n,i)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const a=t.slice(2,-2);return{type:"strong",raw:t,text:a,tokens:this.lexer.inlineTokens(a)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=a(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const n=this.rules.inline.autolink.exec(e);if(n){let e,s;return"@"===n[2]?(e=a(this.options.mangle?t(n[1]):n[1]),s="mailto:"+e):(e=a(n[1]),s=e),{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let n;if(n=this.rules.inline.url.exec(e)){let e,s;if("@"===n[2])e=a(this.options.mangle?t(n[0]):n[0]),s="mailto:"+e;else{let t;do{t=n[0],n[0]=this.rules.inline._backpedal.exec(n[0])[0]}while(t!==n[0]);e=a(n[0]),s="www."===n[1]?"http://"+n[0]:n[0]}return{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const n=this.rules.inline.text.exec(e);if(n){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(n[0]):a(n[0]):n[0]:a(this.options.smartypants?t(n[0]):n[0]),{type:"text",raw:n[0],text:e}}}}const T={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:w,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};T.def=u(T.def).replace("label",T._label).replace("title",T._title).getRegex(),T.bullet=/(?:[*+-]|\d{1,9}[.)])/,T.listItemStart=u(/^( *)(bull) */).replace("bull",T.bullet).getRegex(),T.list=u(T.list).replace(/bull/g,T.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+T.def.source+")").getRegex(),T._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",T._comment=/|$)/,T.html=u(T.html,"i").replace("comment",T._comment).replace("tag",T._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),T.paragraph=u(T._paragraph).replace("hr",T.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",T._tag).getRegex(),T.blockquote=u(T.blockquote).replace("paragraph",T.paragraph).getRegex(),T.normal={...T},T.gfm={...T.normal,table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"},T.gfm.table=u(T.gfm.table).replace("hr",T.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",T._tag).getRegex(),T.gfm.paragraph=u(T._paragraph).replace("hr",T.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",T.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",T._tag).getRegex(),T.pedantic={...T.normal,html:u("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",T._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:w,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:u(T.normal._paragraph).replace("hr",T.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",T.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()};const R={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:w,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:w,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(n="x"+n.toString(16)),s+="&#"+n+";";return s}R._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",R.punctuation=u(R.punctuation).replace(/punctuation/g,R._punctuation).getRegex(),R.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,R.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,R._comment=u(T._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),R.emStrong.lDelim=u(R.emStrong.lDelim).replace(/punct/g,R._punctuation).getRegex(),R.emStrong.rDelimAst=u(R.emStrong.rDelimAst,"g").replace(/punct/g,R._punctuation).getRegex(),R.emStrong.rDelimUnd=u(R.emStrong.rDelimUnd,"g").replace(/punct/g,R._punctuation).getRegex(),R._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,R._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,R._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,R.autolink=u(R.autolink).replace("scheme",R._scheme).replace("email",R._email).getRegex(),R._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,R.tag=u(R.tag).replace("comment",R._comment).replace("attribute",R._attribute).getRegex(),R._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,R._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,R._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,R.link=u(R.link).replace("label",R._label).replace("href",R._href).replace("title",R._title).getRegex(),R.reflink=u(R.reflink).replace("label",R._label).replace("ref",T._label).getRegex(),R.nolink=u(R.nolink).replace("ref",T._label).getRegex(),R.reflinkSearch=u(R.reflinkSearch,"g").replace("reflink",R.reflink).replace("nolink",R.nolink).getRegex(),R.normal={...R},R.pedantic={...R.normal,strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:u(/^!?\[(label)\]\((.*?)\)/).replace("label",R._label).getRegex(),reflink:u(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",R._label).getRegex()},R.gfm={...R.normal,escape:u(R.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+$("a",i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+$("a",i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.escapedEmSt.exec(a));)a=a.slice(0,i.index+i[0].length-2)+"++"+a.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e,E))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e,E))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r,A))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class I{constructor(e){this.options=e||t}code(e,t,n){const s=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,s);null!=t&&t!==e&&(n=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:a(e,!0))+"
\n":"
"+(n?e:a(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,n,s){if(this.options.headerIds){return`${e}\n`}return`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,n){if(null===(e=f(this.options.sanitize,this.options.baseUrl,e)))return n;let s='",s}image(e,t,n){if(null===(e=f(this.options.sanitize,this.options.baseUrl,e)))return n;let s=`${n}":">",s}text(e){return e}}class q{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,n){return""+n}image(e,t,n){return""+n}br(){return""}}class L{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let n=e,s=0;if(this.seen.hasOwnProperty(n)){s=this.seen[e];do{s++,n=e+"-"+s}while(this.seen.hasOwnProperty(n))}return t||(this.seen[e]=s,this.seen[n]=0),n}slug(e,t={}){const n=this.serialize(e);return this.getNextSafeSlug(n,t.dryrun)}}class C{constructor(e){this.options=e||t,this.options.renderer=this.options.renderer||new I,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new q,this.slugger=new L}static parse(e,t){return new C(t).parse(e)}static parseInline(e,t){return new C(t).parseInline(e)}parse(e,t=!0){let n,s,r,i,l,o,a,c,p,u,g,d,f,k,x,m,b,w,y,_="";const $=e.length;for(n=0;n<$;n++)if(u=e[n],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[u.type]&&(y=this.options.extensions.renderers[u.type].call({parser:this},u),!1!==y||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(u.type)))_+=y||"";else switch(u.type){case"space":continue;case"hr":_+=this.renderer.hr();continue;case"heading":_+=this.renderer.heading(this.parseInline(u.tokens),u.depth,h(this.parseInline(u.tokens,this.textRenderer)),this.slugger);continue;case"code":_+=this.renderer.code(u.text,u.lang,u.escaped);continue;case"table":for(c="",a="",i=u.header.length,s=0;s0&&"paragraph"===x.tokens[0].type?(x.tokens[0].text=w+" "+x.tokens[0].text,x.tokens[0].tokens&&x.tokens[0].tokens.length>0&&"text"===x.tokens[0].tokens[0].type&&(x.tokens[0].tokens[0].text=w+" "+x.tokens[0].tokens[0].text)):x.tokens.unshift({type:"text",text:w}):k+=w),k+=this.parse(x.tokens,f),p+=this.renderer.listitem(k,b,m);_+=this.renderer.list(p,g,d);continue;case"html":_+=this.renderer.html(u.text);continue;case"paragraph":_+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;n+1<$&&"text"===e[n+1].type;)u=e[++n],p+="\n"+(u.tokens?this.parseInline(u.tokens):u.text);_+=t?this.renderer.paragraph(p):p;continue;default:{const e='Token with "'+u.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw new Error(e)}}return _}parseInline(e,t){t=t||this.renderer;let n,s,r,i="";const l=e.length;for(n=0;n{"function"==typeof s&&(r=s,s=null);const i={...s},l=function(e,t,n){return s=>{if(s.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+a(s.message+"",!0)+"
    ";return t?Promise.resolve(e):n?void n(null,e):e}if(t)return Promise.reject(s);if(!n)throw s;n(s)}}((s={...P.defaults,...i}).silent,s.async,r);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(function(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}(s),s.hooks&&(s.hooks.options=s),r){const i=s.highlight;let o;try{s.hooks&&(n=s.hooks.preprocess(n)),o=e(n,s)}catch(e){return l(e)}const a=function(e){let n;if(!e)try{s.walkTokens&&P.walkTokens(o,s.walkTokens),n=t(o,s),s.hooks&&(n=s.hooks.postprocess(n))}catch(t){e=t}return s.highlight=i,e?l(e):r(null,n)};if(!i||i.length<3)return a();if(delete s.highlight,!o.length)return a();let c=0;return P.walkTokens(o,(function(e){"code"===e.type&&(c++,setTimeout((()=>{i(e.text,e.lang,(function(t,n){if(t)return a(t);null!=n&&n!==e.text&&(e.text=n,e.escaped=!0),c--,0===c&&a()}))}),0))})),void(0===c&&a())}if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(n):n).then((t=>e(t,s))).then((e=>s.walkTokens?Promise.all(P.walkTokens(e,s.walkTokens)).then((()=>e)):e)).then((e=>t(e,s))).then((e=>s.hooks?s.hooks.postprocess(e):e)).catch(l);try{s.hooks&&(n=s.hooks.preprocess(n));const r=e(n,s);s.walkTokens&&P.walkTokens(r,s.walkTokens);let i=t(r,s);return s.hooks&&(i=s.hooks.postprocess(i)),i}catch(e){return l(e)}}}function P(e,t,n){return M(v.lex,C.parse)(e,t,n)}P.options=P.setOptions=function(e){var n;return P.defaults={...P.defaults,...e},n=P.defaults,t=n,P},P.getDefaults=e,P.defaults=t,P.use=function(...e){const t=P.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const n={...e};if(n.async=P.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=P.defaults.renderer||new I;for(const n in e.renderer){const s=t[n];t[n]=(...r)=>{let i=e.renderer[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.renderer=t}if(e.tokenizer){const t=P.defaults.tokenizer||new S;for(const n in e.tokenizer){const s=t[n];t[n]=(...r)=>{let i=e.tokenizer[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.tokenizer=t}if(e.hooks){const t=P.defaults.hooks||new Z;for(const n in e.hooks){const s=t[n];Z.passThroughHooks.has(n)?t[n]=r=>{if(P.defaults.async)return Promise.resolve(e.hooks[n].call(t,r)).then((e=>s.call(t,e)));const i=e.hooks[n].call(t,r);return s.call(t,i)}:t[n]=(...r)=>{let i=e.hooks[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.hooks=t}if(e.walkTokens){const t=P.defaults.walkTokens;n.walkTokens=function(n){let s=[];return s.push(e.walkTokens.call(this,n)),t&&(s=s.concat(t.call(this,n))),s}}P.setOptions(n)}))},P.walkTokens=function(e,t){let n=[];for(const s of e)switch(n=n.concat(t.call(P,s)),s.type){case"table":for(const e of s.header)n=n.concat(P.walkTokens(e.tokens,t));for(const e of s.rows)for(const s of e)n=n.concat(P.walkTokens(s.tokens,t));break;case"list":n=n.concat(P.walkTokens(s.items,t));break;default:P.defaults.extensions&&P.defaults.extensions.childTokens&&P.defaults.extensions.childTokens[s.type]?P.defaults.extensions.childTokens[s.type].forEach((function(e){n=n.concat(P.walkTokens(s[e],t))})):s.tokens&&(n=n.concat(P.walkTokens(s.tokens,t)))}return n},P.parseInline=M(v.lexInline,C.parseInline),P.Parser=C,P.parser=C.parse,P.Renderer=I,P.TextRenderer=q,P.Lexer=v,P.lexer=v.lex,P.Tokenizer=S,P.Slugger=L,P.Hooks=Z,P.parse=P,P.options,P.setOptions,P.use,P.walkTokens,P.parseInline,C.parse,v.lex; +/*! + * The reveal.js markdown plugin. Handles parsing of + * markdown inside of presentations as well as loading + * of external markdown documents. + */ +const N="__SCRIPT_END__",O=/\[\s*((\d*):)?\s*([\s\d,|-]*)\]/,D={"&":"&","<":"<",">":">",'"':""","'":"'"};return()=>{let e;function t(e){let t=(e.querySelector("[data-template]")||e.querySelector("script")||e).textContent;t=t.replace(new RegExp(N,"g"),"<\/script>");const n=t.match(/^\n?(\s*)/)[1].length,s=t.match(/^\n?(\t*)/)[1].length;return s>0?t=t.replace(new RegExp("\\n?\\t{"+s+"}(.*)","g"),(function(e,t){return"\n"+t})):n>1&&(t=t.replace(new RegExp("\\n? {"+n+"}(.*)","g"),(function(e,t){return"\n"+t}))),t}function n(e){const t=e.attributes,n=[];for(let e=0,s=t.length;e'+P(n[1].trim())+""),' + + + + + diff --git a/docs/training/sparql-basics/index.md b/docs/training/sparql-basics/index.md new file mode 100644 index 000000000..4d90e35f7 --- /dev/null +++ b/docs/training/sparql-basics/index.md @@ -0,0 +1,338 @@ +--- +title: "Training: SPARQL Basics" +icon: material/presentation +tags: + - BeginnersTutorial + - SPARQL +--- +# SPARQL Basics + +
    + +
    + +!!! abstract "About this module" + + This module teaches how to query RDF graphs with SPARQL, from the first triple pattern to aggregation and updates, and ends with exercises on the product Knowledge Graph. + It builds on [RDF and Linked Data Foundations](../rdf-and-linked-data/index.md) and is part of the [Corporate Memory training](../index.md). + + Use **Show slide** next to a heading to bring the matching slide into view, or **Read the explanation** under the slides to jump to the text for the current slide. + All queries run in the [Query module](../../explore-and-author/query-module/index.md) of eccenca Corporate Memory. + +## Why a query language for RDF { #why-a-query-language } + +An RDF graph answers questions only when there is a way to ask them. +Typical questions combine several statements: + +- Which German-language strings occur in the data? – a condition on literals. +- Which relations hold between two given entities? – a variable in the predicate position. +- Which pairs of persons share a parent? – a pattern that joins two statements on a common node. + +A query language for RDF has to express such graph patterns in a readable way, cover schema and instance data alike, and return results in formats that other tools consume. +SPARQL is that language. + +## SPARQL { #sparql } + +SPARQL is a recursive acronym for *SPARQL Protocol and RDF Query Language*. +Version 1.0 became a W3C Recommendation in 2008, version 1.1 in 2013, and 1.1 is what triple stores implement today. + +SPARQL is a family of standards: + +- the **query language**, to read from a graph, +- the **update language**, to change a graph, +- the **protocol**, which defines how a client sends queries to a SPARQL endpoint over HTTP, +- **result formats** in JSON, XML, CSV and TSV. + +In Corporate Memory, SPARQL is everywhere. +The Query module edits and runs queries, the query catalog stores them for reuse, cmemc runs them from the command line, and SHACL forms, charts and custom APIs are driven by queries from the catalog. + +## Structure of a query { #structure-of-a-query } + +A SPARQL query has three parts: + +1. **Prefix declarations** – like `@prefix` in Turtle, but written `PREFIX` and without the closing dot. +2. **The query form and its variables** – `SELECT ?name` returns a table with one column `name`. +3. **The graph pattern** in the `WHERE` block – the conditions a solution has to meet. + +A graph pattern is written like Turtle, with variables in place of some terms. +A variable starts with `?` (or `$`), carries a freely chosen name and may stand in any position: subject, predicate or object. + +The example uses the Registered Organization Vocabulary, whose property `rov:legalName` holds the official name of an organization. +The query reads: *find every `?x` with a legal name, and return the name*. + +## Basic graph patterns { #basic-graph-patterns } + +A set of triple patterns is a basic graph pattern. +The triple store searches for all ways to replace the variables with terms from the graph so that every pattern becomes a triple in the graph. +Each such replacement is one solution, and each solution becomes one row of the result. + +In the example, the variable `?car` occurs in all three patterns. +This joins them: the three triples must share the same subject. +Only `ex:E63_AMG` satisfies all three, and the query returns its model and class. + +Patterns that share no variable are not joined, and the result is the cross product of their solutions. +An unintended cross product is a frequent cause of slow queries with huge results. + +## Matching literals { #matching-literals } + +A literal in a pattern matches only the identical term. +The data contains four literals with the text `test`, but they are not the same term: + +- `"test"` and `"test"^^xsd:string` are the same term in RDF 1.1, because a literal without a datatype is an `xsd:string`. + The query returns both `ex:bsp1` and `ex:bsp2`. +- `"test"@de` is a language-tagged string, a different term. +- `"42"^^xsd:integer` is a number. + +!!! note "Changed since RDF 1.0" + + Older slide versions show only `ex:bsp1` in the result. + That was the behavior under RDF 1.0, where plain literals and `xsd:string` literals were different terms. + +To find a text regardless of its language tag or datatype, compare its string value in a filter: `FILTER (STR(?o) = "test")`. + +## OPTIONAL and UNION { #optional-and-union } + +Braces group patterns. +Two keywords combine groups. + +**`OPTIONAL { … }`** adds information when it exists, without discarding solutions when it does not. +In the customer example, every customer appears, and the variables `?phone` and `?email` stay unbound where the data has no value. +Without `OPTIONAL`, Schmidt, who has neither, would disappear from the result, and so would Meyer and Schulze. + +**`{ … } UNION { … }`** accepts solutions from either of two patterns. +The union is not exclusive: Mueller, who has a phone number and an email address, appears twice, once for each. +Binding both alternatives to the same variable `?contactData` puts phone numbers and email addresses into one column. + +## Combining OPTIONAL and UNION { #combining-optional-and-union } + +`OPTIONAL` and `UNION` bind equally strong and are left-associative: each applies to everything written to its left within the same group. + +This matters in a query such as *for all books published by Springer, show the author or editor and, if available, the first name*. +Written without extra braces, the `OPTIONAL` attaches to the whole union, which is the intended reading. +A reader who expects it to apply only to the editor branch is surprised by the result. + +Explicit braces make the intention visible and cost nothing. + +## FILTER { #filter } + +Graph patterns select by structure. +Filters select by value: participants younger than 30, last names containing a hyphen, texts in German. + +`FILTER (expression)` evaluates the expression for each solution and keeps only those for which it is true. +A solution is removed when the expression is false and also when it raises an error, for example a comparison between a number and a string. + +A filter applies to the whole group it stands in, wherever in the group it is written. + +## Operators { #operators } + +**Comparison operators** – `<`, `=`, `>`, `<=`, `>=`, `!=` – compare literals by their natural order: numbers numerically, dates chronologically, strings alphabetically, and `false` before `true`. +IRIs and blank nodes support only `=` and `!=`. +Values of incompatible types, such as a string and an integer, cannot be compared, and the comparison is an error. + +**Arithmetic operators** – `+`, `-`, `*`, `/` – work on numbers and combine values, as in `FILTER (?weight / (?length * ?height) >= 25)`. + +**Logical operators** – `&&`, `||`, `!` – combine conditions. +SPARQL logic has three values: true, false and error. +`true || error` is true and `false && error` is false, because the other operand already decides the outcome; in every other case an error propagates. +At the end, the filter treats an error like false. + +## Functions for RDF terms { #rdf-functions } + +SPARQL provides functions that inspect RDF terms: + +- `BOUND(?x)` tests whether a variable has a value, typically after `OPTIONAL`. +- `isIRI`, `isBLANK` and `isLITERAL` test the kind of term. +- `STR` returns the lexical form of a literal or IRI, `LANG` its language tag, `DATATYPE` its datatype IRI. +- `langMatches(LANG(?label), "de")` tests a language tag against a language range, so that `de` also matches `de-CH`. +- `REGEX(?text, "pattern", "i")` tests a string against a regular expression, here ignoring case. + +SPARQL 1.1 adds string functions such as `CONTAINS`, `STRSTARTS`, `LCASE`, `CONCAT` and `REPLACE`, which are faster and easier to read than a regular expression for simple cases. + +## Negation { #negation } + +*Show all cars without a price* asks for something that is not in the data. +`FILTER NOT EXISTS { … }` keeps a solution only if the enclosed pattern has no match for it. + +`MINUS { … }` removes the solutions that are compatible with the enclosed pattern. +The two give the same result in most queries, and differ when the patterns share no variable. +`FILTER NOT EXISTS` is the safer default. + +The older idiom `OPTIONAL { … } FILTER (!BOUND(?x))` achieves the same and appears in many existing queries. + +## Solution modifiers { #solution-modifiers } + +Solution modifiers shape the result table after the pattern has been matched: + +- `ORDER BY` sorts the rows. + `ASC()` is the default, `DESC()` reverses, and several keys sort hierarchically. +- `DISTINCT` removes duplicate rows. +- `OFFSET n` skips the first n rows. +- `LIMIT n` returns at most n rows. + +The modifiers apply in this order: sort, remove duplicates, skip, cut. +Without `ORDER BY`, the order of the rows is undefined, and `LIMIT` and `OFFSET` return arbitrary rows. +Combine them with `ORDER BY` to page through a result reliably. + +## Query forms { #query-forms } + +SPARQL has four query forms, which differ in what they return: + +- `SELECT` returns a table of variable bindings, the most common form. +- `ASK` returns true or false, depending on whether the pattern matches at all. + In the example, BARCO has no registered site, so the answer is `false`. +- `DESCRIBE` returns an RDF graph with information about the resources found. + The standard leaves open which triples belong to the description. + Most stores return the triples with the resource as subject, the Concise Bounded Description. +- `CONSTRUCT` returns an RDF graph built from a template. + +The examples use two W3C vocabularies: `rov:` for the Registered Organization Vocabulary and `org:` for the Organization Ontology. + +## CONSTRUCT { #construct } + +`CONSTRUCT` turns query results into new triples. +The template after `CONSTRUCT` is a set of triple patterns, and the triple store fills in the variables for every solution of the `WHERE` block. + +The example derives an `rdfs:label` from each legal name. +Typical uses are mapping data from one vocabulary to another, deriving shortcuts over long paths, and extracting a subgraph for export. +In Corporate Memory, a `CONSTRUCT` query in a workflow writes its result into a graph, which makes it a building block for data transformation. + +## Aggregation and more { #aggregation } + +SPARQL 1.1 added the features that make it a complete analysis language: + +- **Aggregates** – `COUNT`, `SUM`, `MIN`, `MAX`, `AVG`, `SAMPLE`, `GROUP_CONCAT` – summarize the rows of each group formed by `GROUP BY`. + The example counts the products per product manager. +- **Projection of expressions** – `(COUNT(?product) AS ?products)` or `BIND (… AS ?x)` assign a computed value to a new variable. +- **Subqueries** nest a complete `SELECT` inside a pattern. +- **`VALUES`** provides a fixed list of bindings, for example a list of IRIs to look up. +- **Property paths** follow chains and repetitions of properties: `prod:memberOf/rdfs:label` reaches the label of the department, `rdfs:subClassOf*` walks a class hierarchy to any depth. + +## SPARQL Update { #sparql-update } + +SPARQL 1.1 Update changes graphs: + +- `INSERT DATA { … }` and `DELETE DATA { … }` add or remove the given triples. +- `DELETE { … } INSERT { … } WHERE { … }` computes the changes from a pattern, for example to rename a property throughout a graph. +- `CLEAR GRAPH`, `DROP GRAPH`, `LOAD` and `COPY` work on whole graphs. + +`GRAPH { … }` selects the named graph to change. +In Corporate Memory, every graph is a named graph, and write access is controlled per graph by access conditions. + +## Exercises { #exercises } + +The exercises use the product Knowledge Graph built in module 1, see [Lift data from tabular data](../../build/lift-data-from-tabular-data-such-as-csv-xslx-or-database-tables/index.md). +Its vocabulary uses the prefix `prod:` for `http://ld.company.org/prod-vocab/`. +Run the queries in the Query module, against the graph that contains the instance data. + +??? example "1. Find all products" + + ```sparql + PREFIX prod: + PREFIX rdfs: + + SELECT ?product ?label + WHERE { + ?product a prod:Product . + OPTIONAL { ?product rdfs:label ?label . } + } + ORDER BY ?label + ``` + +??? example "2. Find products lighter than 10" + + ```sparql + PREFIX prod: + + SELECT ?product ?weight + WHERE { + ?product a prod:Product ; + prod:weight ?weight . + FILTER (?weight < 10) + } + ``` + + The products vocabulary defines no weight property. + Use the property that the mapping of the `Weigth` column of `products.xlsx` writes to, `prod:weight` is an example. + +??? example "3. Find products and services without a product manager" + + ```sparql + PREFIX prod: + + SELECT ?item + WHERE { + { ?item a prod:Product . } UNION { ?item a prod:Service . } + FILTER NOT EXISTS { ?item prod:hasProductManager ?manager . } + } + ``` + +??? example "4. Find products without a responsible department" + + ```sparql + PREFIX prod: + + SELECT ?product + WHERE { + ?product a prod:Product . + FILTER NOT EXISTS { ?department prod:responsibleFor ?product . } + } + ``` + +??? example "5. Find experts for the category of a product" + + ```sparql + PREFIX prod: + + SELECT ?product ?category ?expert + WHERE { + ?product prod:hasCategory ?category . + ?expert prod:expertFor ?category . + } + ``` + +??? example "6. Count the products per manager and department" + + ```sparql + PREFIX prod: + + SELECT ?department ?manager (COUNT(DISTINCT ?product) AS ?products) + WHERE { + ?product prod:hasProductManager ?manager . + OPTIONAL { ?manager prod:memberOf ?department . } + } + GROUP BY ?department ?manager + ORDER BY ?department DESC(?products) + ``` + + Save this query in the query catalog. + The charts and custom API exercises reuse it. + +??? example "7. Insert yourself as an employee" + + ```sparql + PREFIX prod: + PREFIX rdfs: + + INSERT DATA { + GRAPH { + a prod:Employee ; + rdfs:label "Jane Doe" ; + prod:email "jane.doe@company.org" . + } + } + ``` + + Replace the IRI, label and email address with personal values, and the graph IRI with the instance graph of the project. + +## Summary { #summary } + +| Area | Elements | +| --- | --- | +| Basic structure | `PREFIX`, `WHERE { }` | +| Query forms | `SELECT`, `ASK`, `DESCRIBE`, `CONSTRUCT` | +| Graph patterns | basic graph patterns, `{ }`, `OPTIONAL`, `UNION`, `FILTER NOT EXISTS`, `MINUS` | +| Solution modifiers | `ORDER BY`, `DISTINCT`, `OFFSET`, `LIMIT` | +| Filter functions | `BOUND`, `isIRI`, `isBLANK`, `isLITERAL`, `STR`, `LANG`, `DATATYPE`, `sameTerm`, `langMatches`, `REGEX` | +| SPARQL 1.1 | aggregates, `GROUP BY`, `BIND`, `VALUES`, subqueries, property paths, update | + +The full language is specified in the [SPARQL 1.1 Query Language](https://www.w3.org/TR/sparql11-query/) recommendation. diff --git a/docs/training/sparql-basics/slides.html b/docs/training/sparql-basics/slides.html new file mode 100644 index 000000000..29f608c78 --- /dev/null +++ b/docs/training/sparql-basics/slides.html @@ -0,0 +1,494 @@ + + + + + + +Slides: SPARQL Basics · eccenca Corporate Memory training + + + + + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + + + + + + + diff --git a/mkdocs.yml b/mkdocs.yml index cd9db3e2a..632dac991 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,11 +82,13 @@ extra: extra_css: - assets/fonts.css - assets/extra.css + - assets/training.css extra_javascript: # https://squidfunk.github.io/mkdocs-material/reference/data-tables/#sortable-tables - assets/tablesort.min.js - assets/tablesort.js - assets/extra.js + - assets/training.js # }}} # {{{ Theme configuration diff --git a/nav.yml b/nav.yml index 5a9fdb8bf..40a8b8ab8 100644 --- a/nav.yml +++ b/nav.yml @@ -954,3 +954,9 @@ nav: - release-notes/2019/corporate-memory-19-10/index.md - Tutorials: - tutorials/index.md + - Training: + - Overview: training/index.md + - RDF and Linked Data: + - training/rdf-and-linked-data/index.md + - SPARQL Basics: + - training/sparql-basics/index.md diff --git a/tools/check_zensical_output.py b/tools/check_zensical_output.py index b7d024508..b9d074747 100644 --- a/tools/check_zensical_output.py +++ b/tools/check_zensical_output.py @@ -59,6 +59,7 @@ ) # and friends are metadata, not asset loads. LINK_META_RE = re.compile(r'rel="(canonical|alternate|manifest)"', re.IGNORECASE) +DOCS_DIR = Path("docs") OG_IMAGE_RE = re.compile(r' None: # Redirect stubs and the 404 page get no card. if REFRESH_RE.search(body) or page == site / "404.html": continue + # An .html file in docs/ is copied verbatim, not rendered as a page, + # e.g. the slide decks of the training modules - it gets no card either. + if (DOCS_DIR / page.relative_to(site)).is_file(): + continue found = OG_IMAGE_RE.search(body) if not found: missing.append(page.relative_to(site).as_posix()) From 4e4c0b7fb0b092e590fe0a58be6b99bd150a64b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Annama=CC=81ria=20Koren?= Date: Mon, 9 Nov 2026 10:50:48 +0100 Subject: [PATCH 2/2] add ontology training modules and group tutorials under Learn, CMEM-8382 - add the modules Ontology Modeling and Ontology Engineering, with slides and an explanation per slide - rewrite the SPARQL exercise solutions against the Product Data Demo package: no inference on prod:Product, prod:weight_g, prod:areaOfExpertise; two exercises returned no rows before - merge the Tutorials and Training tabs into one Learn tab and show the navigation on the tutorials page Verified against CMEM v26.2.2. --- docs/.pages | 6 +- docs/assets/revealjs/eccenca-theme.css | 14 + docs/training/.pages | 2 + docs/training/index.md | 17 +- docs/training/ontology-engineering/index.md | 213 +++++++ .../training/ontology-engineering/slides.html | 284 ++++++++++ docs/training/ontology-modeling/index.md | 300 ++++++++++ docs/training/ontology-modeling/slides.html | 524 ++++++++++++++++++ docs/training/rdf-and-linked-data/index.md | 5 +- docs/training/sparql-basics/index.md | 110 +++- docs/training/sparql-basics/slides.html | 19 +- docs/tutorials/index.md | 2 - nav.yml | 21 +- 13 files changed, 1461 insertions(+), 56 deletions(-) create mode 100644 docs/training/ontology-engineering/index.md create mode 100644 docs/training/ontology-engineering/slides.html create mode 100644 docs/training/ontology-modeling/index.md create mode 100644 docs/training/ontology-modeling/slides.html diff --git a/docs/.pages b/docs/.pages index 61a95a63e..f39944595 100644 --- a/docs/.pages +++ b/docs/.pages @@ -8,6 +8,6 @@ nav: - Automate: automate - Develop: develop - Release Notes: release-notes - - Tutorials: tutorials - - Training: training - + - Learn: + - Tutorials: tutorials + - Training: training diff --git a/docs/assets/revealjs/eccenca-theme.css b/docs/assets/revealjs/eccenca-theme.css index 9a7d7d796..d56ccf85b 100644 --- a/docs/assets/revealjs/eccenca-theme.css +++ b/docs/assets/revealjs/eccenca-theme.css @@ -301,3 +301,17 @@ right: auto; bottom: 14px; } + +/* ontology engineering: marking concepts, relations, instances and attributes in a text */ +.reveal .domain-text { font-size: 0.85em; line-height: 1.5; } +.reveal .c-concept { color: #3c6fb0; font-weight: 500; } +.reveal .c-rel { color: #c0504d; font-weight: 500; } +.reveal .c-inst { color: #7030a0; font-style: italic; } +.reveal .c-attr { color: #2e8b57; font-weight: 500; } + +/* class diagrams */ +.reveal svg.diagram .cls { fill: #4f81bd; stroke: #2e5a8a; } +.reveal svg.diagram .cls-label { fill: #fff; } +.reveal svg.diagram .rel { stroke: #e08a00; stroke-width: 2; fill: none; } +.reveal svg.diagram .rel-label { fill: #c97800; } +.reveal svg.diagram .sub { stroke: #4f81bd; stroke-width: 1.5; fill: none; } diff --git a/docs/training/.pages b/docs/training/.pages index cef2cd2a4..534dc7fe7 100644 --- a/docs/training/.pages +++ b/docs/training/.pages @@ -3,3 +3,5 @@ nav: - Overview: index.md - RDF and Linked Data: rdf-and-linked-data - SPARQL Basics: sparql-basics + - Ontology Modeling: ontology-modeling + - Ontology Engineering: ontology-engineering diff --git a/docs/training/index.md b/docs/training/index.md index 87c00b629..51a0f58d3 100644 --- a/docs/training/index.md +++ b/docs/training/index.md @@ -12,7 +12,7 @@ Each module combines the slides with a written explanation of every slide, so th !!! info "Draft" This section is a draft. - It contains the first two theory modules, the remaining modules follow. + It contains the four theory modules, material for the product-related sessions follows. ## Modules @@ -30,6 +30,18 @@ Each module combines the slides with a written explanation of every slide, so th Graph patterns, filters, solution modifiers, query forms, aggregation and updates, with exercises on the product Knowledge Graph. +- :material-sitemap-outline: **[Ontology Modeling](ontology-modeling/index.md)** + + --- + + Classes, properties and inference with RDF Schema, and the richer axioms of OWL. + +- :material-pencil-ruler-outline: **[Ontology Engineering](ontology-engineering/index.md)** + + --- + + Hands-on: from competency questions and a domain description to a formal vocabulary. + ## Two-day training @@ -43,7 +55,8 @@ Theory alternates with hands-on sessions in Corporate Memory, which use the tuto | --- | --- | | Introduction to eccenca Corporate Memory: build, explore, consume and govern | [Getting Started](../getting-started/index.md) | | RDF and Linked Data foundations | [Module](rdf-and-linked-data/index.md) | -| Ontology modeling with RDF Schema and OWL | Module in preparation | +| Ontology modeling with RDF Schema and OWL | [Module](ontology-modeling/index.md) | +| Hands-on: ontology engineering for the product domain | [Module](ontology-engineering/index.md), [Visually Authoring Ontologies](../explore-and-author/bke-module/visually-authoring-ontologies/index.md) | | Hands-on: register the products vocabulary, lift the hardware, services and organization data, combine them in a workflow | [Lift data from tabular data](../build/lift-data-from-tabular-data-such-as-csv-xslx-or-database-tables/index.md), [Lift data from JSON and XML sources](../build/lift-data-from-json-and-xml-sources/index.md), [Workflows](../build/workflows/index.md) | | Hands-on: link similar products, try active learning | [Active learning](../build/active-learning/index.md) | | Hands-on: schedule a workflow | [Scheduling workflows](../automate/scheduling-workflows/index.md) | diff --git a/docs/training/ontology-engineering/index.md b/docs/training/ontology-engineering/index.md new file mode 100644 index 000000000..1d17ff693 --- /dev/null +++ b/docs/training/ontology-engineering/index.md @@ -0,0 +1,213 @@ +--- +title: "Training: Ontology Engineering" +icon: material/presentation +tags: + - BeginnersTutorial + - Vocabulary +--- +# Ontology Engineering + +
    + +
    + +!!! abstract "About this module" + + This hands-on module shows how to develop an ontology for a domain, from the first requirements to a formal vocabulary in eccenca Corporate Memory. + The example is the product domain whose data the [SPARQL exercises](../sparql-basics/index.md#exercises) query. + It follows [Ontology Modeling](../ontology-modeling/index.md) and is part of the [Corporate Memory training](../index.md). + + Use **Show slide** next to a heading to bring the matching slide into view, or **Read the explanation** under the slides to jump to the text for the current slide. + +## What an ontology is { #what-is-an-ontology } + +An ontology is a shared, structured conceptualization of a domain. +Each of the three words carries part of the meaning: + +- **Conceptualization** – an abstract, simplified view of the domain, made for a purpose. + It captures the objects and concepts of the domain and the relations between them. +- **Structured** – written in a standard modeling language such as RDF Schema, OWL or SKOS, so that machines can process it. +- **Shared** – agreed on by the domain experts, so that it serves as a common, extensible standard for the data of the domain. + +Domains range from the most basic things, such as persons, places and activities, to specialist areas such as supply chains or manufacturing. + +## Methodology { #methodology } + +There is no single correct way to engineer an ontology, and no single correct result. +The domain and the application decide. + +A few principles hold in general. +The concepts of the ontology stay close to the physical or logical objects of the domain and the relations between them. +They are usually the nouns and verbs of sentences that describe the domain: nouns become classes, verbs become properties. +The intended use decides which of them the ontology needs. + +Ontology engineering is iterative. +A first model is discussed with the domain experts, applied to real data, and refined, again and again. + +## Scope and requirements { #scope-and-requirements } + +The scope of an ontology depends on its domain, its application and its users. +A few questions clarify it at the start: + +- Is there data already, or is the model built on a green field? +- If there is data, what is it used for? +- Is the goal a clearer, logical model of the domain, or compatibility with existing structures? +- How much metadata is needed? +- Which other data sources are relevant? +- Do ontologies for the domain exist already, see [Reuse existing vocabularies](../ontology-modeling/index.md#reuse)? + +## Competency questions { #competency-questions } + +Competency questions turn the requirements into a test. +They are the questions the ontology has to be able to answer, collected in a catalog at the start. + +After each milestone, example data that follows the ontology is created, and the questions are answered by querying or browsing it. +A question that cannot be answered shows a gap in the model. + +For the product domain, typical competency questions are: + +- Which products is a department responsible for? +- Who manages a given product, and in which department does that person work? +- Who can take over a product when its manager leaves? + +The [SPARQL exercises](../sparql-basics/index.md#exercises) answer exactly such questions on the product data. + +## Source material { #source-material } + +The knowledge for an ontology comes from many sources: +existing databases and spreadsheets, slide decks and training material, company taxonomies, product catalogs, books and standards, and interviews with domain experts. + +In practice, it is usually a combination. +A project receives a spreadsheet and a slide deck, interviews a domain expert, drafts a first ontology, iterates it with the expert, and receives further material along the way. + +## The domain { #the-domain } + +The exercise models a short description of a product domain: +products and services, the product managers who manage them, the departments the managers work in, and the product categories in which the departments and managers have expertise. + +The second paragraph of the description names concrete things: a product with its identifier and name, a product manager and a department. +These are the instances the model has to accommodate. + +## Concepts, relationships, instances and attributes { #concepts-relationships-instances-and-attributes } + +Reading the description four times, each time with a different question, structures the domain: + +1. **Concepts** – what kinds of things are there? + Products, services, product managers, departments, direct reports and categories. +2. **Relationships** – how are they connected? + Products are *managed by* product managers, managers *have* direct reports, products are *structured into* categories, departments are *responsible* for products, and managers and departments *have expertise* in categories. +3. **Instances** – which concrete things are mentioned? + P516-8211068, Film Multiplexer Rheostat Warp, Kristen Bauers and Procurement. + In a real project, instances come from the data sources rather than from documentation. +4. **Attributes** – which values describe a thing? + A product is *named*, so it has a name. + +## Defining classes { #defining-classes } + +Three strategies lead from the candidates to a class hierarchy: + +- **Top-down** starts with the most general classes and specializes them. +- **Bottom-up** starts with the most specific classes and groups them into more general ones. +- **Middle-out** starts with the most important classes and generalizes and specializes from there. + +In the product domain, middle-out works well. +*Product* is central, and *hardware* and *service* specialize it. +*Product manager* and *direct report* turn out not to be classes of their own: both are employees, distinguished only by their relations. +They become the class *Employee*, its subclass *Manager*, and the common superclass *Agent* for anyone who can be a member of a department. + +A drawing helps at this stage: classes as boxes, relations as arrows between them. + +## The model { #the-model } + +The diagram shows the core of the products vocabulary as it is installed with the **Product Data Demo** package, version v1.2.0: + +| Class | Meaning | +| --- | --- | +| `prod:Product` with `prod:Hardware` and `prod:Service` | what the company offers | +| `prod:Agent` with `prod:Employee` and `prod:Manager` | people who act in the company | +| `prod:Department` | an organizational unit | +| `prod:ProductCategory` | a category of products | + +| Property | From | To | +| --- | --- | --- | +| `prod:hasProductManager` | Product | Employee | +| `prod:hasCategory` | Product | ProductCategory | +| `prod:areaOfExpertise` | Agent | ProductCategory | +| `prod:memberOf` | Agent | Department | +| `prod:responsibleFor` | Department | Product | +| `prod:hasManager` | Employee | Manager | +| `prod:eligibleFor` | Service | Product | + +The relations of the description map onto these properties: *managed by* became `prod:hasProductManager`, *have expertise* became `prod:areaOfExpertise`. +The direct report relation is `prod:hasManager`, read from the other end, and `prod:hasDirectReport` exists as well. + +The vocabulary has grown beyond the first draft of the exercise. +It adds suppliers, prices, bills of material and physical dimensions, and models addresses as simple attributes of an agent. + +!!! note "Older vocabulary versions" + + Earlier versions of this exercise and of the build tutorials use the vocabulary version 0.0.1. + It names the expertise property `prod:expertFor`, has a property `prod:managesProduct`, and models addresses as a class `prod:Address` of their own. + +## Class or attribute? { #class-or-attribute } + +A recurring modeling decision is whether something becomes a class with instances of its own, or remains an attribute value. + +Take facilities and the cities they are in. +As long as the city is only a name, an attribute such as `cityName` with a string value is enough. +Once the data says something about the city itself, such as its location, the city has to become a resource: the facility is `locatedIn` a city, and the city has a label and a location. + +Make something a class when + +- it is a business object in its own right, +- there are, or will be, statements about it, +- other datasets describe the same things, so that links to them become possible. + +Both options are valid, and the requirements decide. + +## Formalize { #formalize } + +The drawing is then written down as RDFS and OWL. +Each class and property gets a type, its place in the hierarchy, domain and range, a label, and a link to the ontology that defines it. + +Any text editor can produce Turtle. +Ontology editors such as Protégé add forms, consistency checks and reasoning. +In Corporate Memory, the Business Knowledge Editor creates classes and properties visually, see [Visually Authoring Ontologies](../../explore-and-author/bke-module/visually-authoring-ontologies/index.md). + +## Quality checks { #quality-checks } + +An ontology is easier to use and maintain when it meets a few rules consistently: + +- The ontology header names the version, the preferred namespace and the preferred prefix. +- Every class and property has an `rdf:type` and an `rdfs:isDefinedBy` pointing to the ontology. +- Every class and property has at least one `rdfs:label` and one `rdfs:comment`, without placeholders such as "todo". +- Labels differ from comments, and no two resources share a label or a comment. + +Such rules can be checked automatically with SPARQL queries or SHACL shapes. +SHACL also checks the instance data against the model, and Corporate Memory can run these checks in workflows and in continuous integration pipelines, see [Continuous integration](../../automate/continuous-integration/index.md). + +## Visualize and use { #visualize-and-use } + +A diagram shows the structure of an ontology at a glance. +It helps domain experts, who often find a picture easier to follow than Turtle, and it reveals modeling errors such as missing relations. + +Corporate Memory supports the whole cycle: + +- the [Vocabulary Catalog](../../explore-and-author/vocabulary-catalog/index.md) installs and manages vocabularies, +- the [Business Knowledge Editor](../../explore-and-author/bke-module/index.md) explores and authors ontologies visually, +- the mapping editor transforms data from spreadsheets, databases and other sources into instances of the ontology, see [Lift data from tabular data](../../build/lift-data-from-tabular-data-such-as-csv-xslx-or-database-tables/index.md). + +An ontology without data stays theory. +Mapping the sources onto it fills it with life. + +## Exercise { #exercise } + +Develop a small ontology, either for a domain from daily work or by extending the product domain, for example with suppliers: + +1. Write five competency questions. +2. Describe the domain in a few sentences, and mark the concepts, relations, instances and attributes. +3. Draw the model. +4. Formalize it in Turtle, or create it in the Business Knowledge Editor. +5. Create a few instances, and answer the competency questions with SPARQL. + +Compare the result with the products vocabulary of the Product Data Demo, which models suppliers with the class `prod:Supplier` and the property `prod:hasSupplier`. diff --git a/docs/training/ontology-engineering/slides.html b/docs/training/ontology-engineering/slides.html new file mode 100644 index 000000000..959685187 --- /dev/null +++ b/docs/training/ontology-engineering/slides.html @@ -0,0 +1,284 @@ + + + + + + +Slides: Ontology Engineering · eccenca Corporate Memory training + + + + + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +

    The model

    + + + + + + + + ProductCategory + Product + Hardware + Service + Agent + Employee + Manager + Department + + + + + + + hasCategory + areaOfExpertise + hasProductManager + memberOf + responsibleFor + hasManager + eligibleFor + + +

    Core of the products vocabulary v1.2.0 (prefix prod:) – attributes such as name, id and email omitted.

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + + + + + + + diff --git a/docs/training/ontology-modeling/index.md b/docs/training/ontology-modeling/index.md new file mode 100644 index 000000000..c05bdf086 --- /dev/null +++ b/docs/training/ontology-modeling/index.md @@ -0,0 +1,300 @@ +--- +title: "Training: Ontology Modeling" +icon: material/presentation +tags: + - BeginnersTutorial + - Vocabulary +--- +# Ontology Modeling + +
    + +
    + +!!! abstract "About this module" + + This module introduces RDF Schema and OWL, the two W3C languages for describing the classes and properties of a domain. + It builds on [RDF and Linked Data Foundations](../rdf-and-linked-data/index.md) and [SPARQL Basics](../sparql-basics/index.md), and prepares the hands-on module [Ontology Engineering](../ontology-engineering/index.md). + It is part of the [Corporate Memory training](../index.md). + + Use **Show slide** next to a heading to bring the matching slide into view, or **Read the explanation** under the slides to jump to the text for the current slide. + +## Schema knowledge { #schema-knowledge } + +RDF states facts about individual resources: Einstein discovered the theory of relativity. +It has no means of saying what kinds of things exist and how they relate in general. +That only persons discover things, or that every researcher is a person, is knowledge about the schema of the data, also called terminological knowledge. + +Schema knowledge is what a vocabulary contains. +Written down in RDF Schema or OWL, it serves three purposes: +it documents the meaning of the data, it lets software such as the mapping and form editors of eccenca Corporate Memory propose suitable classes and properties, and it allows a reasoner to derive facts that are only implied. + +## RDF Schema { #rdf-schema } + +RDF Schema, RDFS for short, is part of the W3C RDF recommendations, and RDF Schema 1.1 from 2014 is the current version. +It provides a small set of terms for describing classes and properties. + +RDFS is itself written in RDF: a schema is a set of triples, stored and queried like any other data. +Its terms are generic and fit any domain. +They live in the namespace `http://www.w3.org/2000/01/rdf-schema#`, usually abbreviated `rdfs:`. + +## Classes and instances { #classes-and-instances } + +A class is a set of things, and is named by an IRI. +Declaring a class means stating that its IRI is an `rdfs:Class`. +The property `rdf:type`, written `a` in Turtle, makes a resource an instance of a class, and a resource can be an instance of any number of classes. + +Nothing in the syntax separates classes from instances: both are IRIs. +Only the statements about an IRI show whether it is used as a class, as an instance, or as both. + +## Class hierarchies { #class-hierarchies } + +`rdfs:subClassOf` arranges classes in a hierarchy. +`ex:Car rdfs:subClassOf ex:MotorVehicle` means that every car is a motor vehicle. +A class can have several superclasses, so the hierarchy is a directed graph rather than a strict tree. + +The products vocabulary used in the exercises has two such hierarchies: +`prod:Hardware` and `prod:Service` are subclasses of `prod:Product`, and `prod:Manager` is a subclass of `prod:Employee`, which in turn is a subclass of `prod:Agent`. + +## Inference { #inference } + +Schema knowledge makes implicit facts derivable. +From *the blue VW Golf is a car* and *every car is a motor vehicle* it follows that the blue VW Golf is a motor vehicle. +The RDFS semantics defines a fixed set of such entailment rules. + +`rdfs:subClassOf` is transitive: a car is a motor vehicle, a motor vehicle is a vehicle, so a car is a vehicle. +When two classes are each other's subclass, they have exactly the same instances and are therefore equivalent. + +!!! info "Inference in the triple store" + + Whether a query sees inferred triples depends on the triple store and its configuration. + In the product data of the exercises, the store does not infer them: a query for `a prod:Product` finds no product, because the data only states `prod:Hardware` and `prod:Service`. + The SPARQL property path `a/rdfs:subClassOf*` evaluates the class hierarchy inside the query instead, see the [SPARQL exercises](../sparql-basics/index.md#exercises). + +## Built-in classes { #built-in-classes } + +RDF and RDFS predefine a few classes: + +- `rdfs:Resource` contains everything. +- `rdfs:Class` contains all classes, and is therefore an instance of itself. +- `rdf:Property` contains all properties. +- `rdfs:Literal` contains all literal values, `rdfs:Datatype` all datatypes, and `rdf:langString` all language-tagged strings. +- `rdf:Statement` contains reified statements, see [Statements about statements](#reification). +- `rdfs:Container` is the superclass of the container classes `rdf:Bag`, `rdf:Seq` and `rdf:Alt`. + +## Properties and their hierarchies { #properties } + +A property relates two things. +Mathematically, it is a set of pairs: the property *drives* is the set of all pairs of a driver and a vehicle. +A property is declared as an instance of `rdf:Property`. + +`rdfs:subPropertyOf` arranges properties in a hierarchy, just like classes. +If driving is a way of controlling, then every pair in *drives* is also a pair in *controls*, and from *Max drives the Golf* follows *Max controls the Golf*. +A vocabulary is always an idealized model: in reality, not every driver is in control of the vehicle. + +## Domain and range { #domain-and-range } + +`rdfs:domain` states the class of the subjects of a property, `rdfs:range` the class or datatype of its objects. +For a property with a literal value, the range is a datatype such as `xsd:nonNegativeInteger`. + +In RDFS, domain and range are inference rules. +When Max drives the Golf and the domain of *drives* is *Person*, a reasoner concludes that Max is a person. + +Domain and range also serve as documentation. +They tell modelers and tools which properties fit a class. + +## Domain and range are not constraints { #domain-and-range-are-not-constraints } + +Because domain and range infer, they never reject data. +In the classic example, the range of *belongs to* is *Organization*. +The statement *five belongs to the primes* is then not an error, but turns the primes into an organization. + +The product data shows the same effect. +The range of `prod:hasProductManager` is `prod:Employee`, yet 49 products name a product manager who does not appear in the organization map. +A reasoner would type these three managers as employees, and the gap in the data would be invisible. + +Checking that data fits the schema is the task of SHACL, see [Open world](#open-world). + +## Annotations { #annotations } + +RDFS provides four properties that describe resources for people rather than for reasoners: + +- `rdfs:label` gives a human-readable name, ideally one per language. +- `rdfs:comment` gives a longer description or definition. +- `rdfs:seeAlso` points to further information about the resource. +- `rdfs:isDefinedBy` points to the vocabulary that defines the resource. + +Corporate Memory shows labels and comments wherever a class or property appears, in forms, mapping suggestions and the graph explorer. +Every class and property of a vocabulary should therefore have both. + +## Statements about statements { #reification } + +Sometimes a statement is itself the subject of a statement: a detective supposes that the butler killed the gardener, and this theory is unproven. +RDF reification describes a statement as a resource of type `rdf:Statement`, with the properties `rdf:subject`, `rdf:predicate` and `rdf:object`. + +The described triple is not asserted. +The graph contains the information that someone supposes the butler killed the gardener, not that the butler did. + +Reification is verbose, and querying it is cumbersome. +RDF 1.2 introduces triple terms as a compact alternative. +Corporate Memory supports annotations on statements, see [Statement annotations](../../explore-and-author/graph-exploration/statement-annotations/index.md). + +## The limits of RDF Schema { #limits-of-rdf-schema } + +RDFS has no negation, no cardinalities and no alternatives. +It cannot say that cats are not dogs, that a person has at most one spouse, or that the range of a property is a car or a ship. + +Two range statements are a common pitfall. +`ex:drives rdfs:range ex:Car , ex:Ship` does not mean car or ship, it means that everything driven is a car and a ship at the same time. +In RDFS, the way out is a common superclass, such as *Vehicle*, used as the range. + +RDFS also cannot state that two IRIs name the same individual, or that one property is the inverse of another. +These gaps are filled by OWL. + +## OWL { #owl } + +OWL, the Web Ontology Language, became a W3C Recommendation in 2004, and OWL 2 followed in 2009. +OWL extends RDFS with constructs from description logic, with a precisely defined semantics. + +An OWL ontology is a set of axioms, and every axiom is written as RDF triples. +Like RDFS, OWL distinguishes individuals, the things of the domain, classes, the sets of things, and properties, the relations between things or between things and values. + +Besides Turtle and RDF/XML, OWL has its own syntaxes: OWL/XML for XML tools, the Manchester syntax that ontology editors such as Protégé display, and the functional syntax that shows the formal structure. +In Corporate Memory, ontologies are graphs, imported and exported as Turtle or another RDF format. + +## The ontology header { #ontology-header } + +An ontology describes itself with a resource of type `owl:Ontology`, usually named by the namespace IRI. +The header holds the metadata of the ontology: label and description, creator and dates, the version, and the preferred prefix and namespace. + +The products vocabulary declares `pv` as its preferred prefix. +This training writes `prod:`, which names the same namespace: a prefix is only an abbreviation. + +`owl:imports` includes another ontology, so that its classes and properties are available together with the importing one. +Versioning properties such as `owl:versionInfo`, `owl:priorVersion` and `owl:deprecated` track how an ontology changes. + +The [Vocabulary Catalog](../../explore-and-author/vocabulary-catalog/index.md) of Corporate Memory reads this header to list and manage the installed vocabularies. + +## Classes and properties in OWL { #owl-classes-and-properties } + +OWL declares classes with `owl:Class` and distinguishes two kinds of properties: + +- An `owl:ObjectProperty` relates two resources, for example an employee to a department. +- An `owl:DatatypeProperty` relates a resource to a literal value, for example an employee to an email address. + +The distinction matters when mapping data in Corporate Memory: a value mapping writes a datatype property, an object mapping an object property. + +OWL predefines the classes `owl:Thing`, which contains every individual, and `owl:Nothing`, which contains none. + +## Individuals { #individuals } + +Individuals are typed with `rdf:type` as in RDFS, and properties can have any number of values: Einstein belongs to Princeton and to Bern. + +`owl:sameAs` states that two IRIs name the same individual. +Everything said about one then holds for the other. +This is how OWL links records of the same entity from different sources. + +OWL makes no unique name assumption: two different IRIs may name the same thing unless the data states otherwise. +`owl:differentFrom` and `owl:AllDifferent` state explicitly that individuals are distinct. + +## Relations between classes { #class-relations } + +Three axioms relate classes: + +- `rdfs:subClassOf` – every member of A is a member of B. +- `owl:equivalentClass` – A and B have exactly the same members. +- `owl:disjointWith` – no individual belongs to both. + +Disjointness is inherited. +If faculty staff and publications are disjoint, and professors are faculty staff and books are publications, then professors and books are disjoint as well. +An individual typed as both would make the ontology inconsistent, and a reasoner reports the contradiction. + +## Building classes from classes { #class-constructors } + +OWL builds new classes from existing ones: + +- `owl:intersectionOf` – members of all listed classes: a moon of Mars is a moon and an object near Mars. +- `owl:unionOf` – members of at least one listed class: a person is an adult or a child. +- `owl:complementOf` – everything not in the class. +- `owl:oneOf` – exactly the listed individuals: the moons of Mars are Phobos and Deimos. +- `owl:disjointUnionOf` – a union whose parts do not overlap: every professor is either active or retired. + +Such class expressions usually appear as anonymous classes in square brackets, combined with `owl:equivalentClass` or `rdfs:subClassOf`. +The lists are RDF collections, written in parentheses without commas. + +## Property characteristics { #property-characteristics } + +OWL describes properties in more detail than RDFS: + +- `owl:inverseOf` – *has manager* and *has direct report* are inverse, so each statement implies the other in the opposite direction. +- `owl:TransitiveProperty` – a part of a part is a part. +- `owl:SymmetricProperty` – a marriage holds in both directions. +- `owl:FunctionalProperty` – a property with at most one value, such as a birth date. +- `owl:equivalentProperty` – two properties with the same pairs, typically from different vocabularies. + +The products vocabulary defines `prod:hasManager` and `prod:hasDirectReport` but does not declare them inverse. +The example on the slide shows how it could. + +## Property restrictions { #restrictions } + +A restriction is an anonymous class defined by the values of a property: + +- `owl:allValuesFrom` – all values come from a class: every examiner of an exam is a professor. +- `owl:someValuesFrom` – at least one value comes from a class: every exam has at least one examiner who is a person. +- `owl:hasValue` – a specific value is present. +- `owl:minCardinality`, `owl:maxCardinality`, `owl:cardinality` – the number of values: an exam has at most two examiners. + +Stating that a class is a subclass of a restriction makes the restriction hold for all its members. + +## Open world { #open-world } + +OWL assumes an open world: what the data does not state may still be true. +If every exam must have an examiner and an exam in the data has none, OWL does not report an error. +It concludes that an examiner exists who has not been recorded. + +For data quality this is rarely the desired behavior. +SHACL, the Shapes Constraint Language, assumes a closed world and checks the data as it is: the exam without examiner violates the shape. + +Corporate Memory uses both: vocabularies in RDFS and OWL describe the meaning of the data, SHACL shapes drive the forms and validate the data, see [Building a customized user interface](../../explore-and-author/graph-exploration/building-a-customized-user-interface/index.md). + +## Expressivity has a price { #expressivity } + +Modeling languages range from simple taxonomies and thesauri over RDFS and OWL to first-order logic and natural language. +The more a language can express, the harder it is to reason with. + +OWL 2 balances the two with several variants: + +- **OWL 2 Full** allows any use of the constructs, but reasoning is undecidable. +- **OWL 2 DL** restricts the use so that reasoning is decidable; it corresponds to the description logic *SROIQ*. +- **OWL 2 EL, QL and RL** are profiles that give up expressivity for reasoning in polynomial time, tuned for large class hierarchies, query rewriting and rule engines respectively. + +OWL 1 had the variants Lite, DL and Full, which older material still mentions. + +Most enterprise vocabularies need little of this: classes, a hierarchy, object and datatype properties with domain and range, and good labels. + +## Reuse existing vocabularies { #reuse } + +Many concepts are already modeled in established vocabularies: + +- **Dublin Core** for metadata such as title, creator and date, +- **FOAF** for people and their relations, +- **SKOS** for thesauri and taxonomies, used by the Corporate Memory [Thesauri management](../../explore-and-author/thesauri-management/index.md), +- the **Organization Ontology** for organizational structures, +- **PROV-O** for provenance, +- **schema.org** for structured data on the Web. + +Reusing them saves modeling work, and makes the data understandable to others who know these vocabularies. +A domain vocabulary can extend them with its own subclasses and subproperties. +The [Vocabulary Catalog](../../explore-and-author/vocabulary-catalog/index.md) installs common vocabularies into Corporate Memory. + +## Summary { #summary } + +RDF Schema describes classes and properties, their hierarchies, domains and ranges, and annotates them for people. +It is enough for a lightweight ontology. +OWL adds equivalence, disjointness, inverse and other property characteristics, class constructors and restrictions. + +Both infer rather than check. +For validating data, Corporate Memory uses SHACL. + +A widely cited definition, by Studer, Benjamins and Fensel in 1998 after Thomas Gruber, sums up the goal: an ontology is a formal, explicit specification of a shared conceptualization. +How to arrive at such a specification is the topic of the hands-on module [Ontology Engineering](../ontology-engineering/index.md). diff --git a/docs/training/ontology-modeling/slides.html b/docs/training/ontology-modeling/slides.html new file mode 100644 index 000000000..b5f2f07db --- /dev/null +++ b/docs/training/ontology-modeling/slides.html @@ -0,0 +1,524 @@ + + + + + + +Slides: Ontology Modeling · eccenca Corporate Memory training + + + + + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +

    Class hierarchies

    +
    +
    +

    rdfs:subClassOf – every instance of the subclass is an instance of the superclass.

    +
    ex:Car rdfs:subClassOf ex:MotorVehicle .
    +ex:Bus rdfs:subClassOf ex:MotorVehicle .
    +ex:MyBlueVWGolf a ex:Car .
    +
    +
    + + + MotorVehicle + Car + Bus + + MyBlueVWGolf + + instance + + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + + + + + + + diff --git a/docs/training/rdf-and-linked-data/index.md b/docs/training/rdf-and-linked-data/index.md index 143f54766..43cbc46f5 100644 --- a/docs/training/rdf-and-linked-data/index.md +++ b/docs/training/rdf-and-linked-data/index.md @@ -271,7 +271,7 @@ The property `rdf:type` states that a resource is an instance of a class. It is used so often that Turtle offers the keyword `a` as shorthand, and both lines on the slide are the same triple. A resource can belong to any number of classes: the blue VW Golf is a car and a German product at the same time. -How classes form hierarchies, and how properties are tied to classes, is the subject of the ontology module. +How classes form hierarchies, and how properties are tied to classes, is the subject of the module [Ontology Modeling](../ontology-modeling/index.md). ## n-ary relations { #n-ary-relations } @@ -319,10 +319,11 @@ RDF is a widely supported standard for storing and exchanging data, and triple s Because every resource has a global name, data from many sources merges into one graph. Plain RDF describes individuals, but it has little to say about their kinds. -That a car is a motor vehicle, or that only a person drives, is schema knowledge, described with RDF Schema and OWL. +That a car is a motor vehicle, or that only a person drives, is schema knowledge, described with RDF Schema and OWL, see [Ontology Modeling](../ontology-modeling/index.md). Getting answers out of a graph needs a query language, SPARQL. Continue with: - the module [SPARQL basics](../sparql-basics/index.md), +- the module [Ontology Modeling](../ontology-modeling/index.md), - the tutorial [Lift data from tabular data](../../build/lift-data-from-tabular-data-such-as-csv-xslx-or-database-tables/index.md), which builds a Knowledge Graph from the data used in the hands-on exercises. diff --git a/docs/training/sparql-basics/index.md b/docs/training/sparql-basics/index.md index 4d90e35f7..ae95d2cf4 100644 --- a/docs/training/sparql-basics/index.md +++ b/docs/training/sparql-basics/index.md @@ -220,9 +220,18 @@ In Corporate Memory, every graph is a named graph, and write access is controlle ## Exercises { #exercises } -The exercises use the product Knowledge Graph built in module 1, see [Lift data from tabular data](../../build/lift-data-from-tabular-data-such-as-csv-xslx-or-database-tables/index.md). -Its vocabulary uses the prefix `prod:` for `http://ld.company.org/prod-vocab/`. -Run the queries in the Query module, against the graph that contains the instance data. +The exercises query the product data of the **Product Data Demo** package, ID `ecc-product-data-project`. +The [sandbox](../../getting-started/with-your-sandbox/index.md) comes with it installed, on another instance it is installed from the [Marketplace](../../distribution/marketplace/index.md). +The package contains the build project *Product Data Integration Demo* and the graphs it generates, among them hardware, services and the organization map, plus the integration graph *Products - Integration* (`http://ld.company.org/prod-inst/`), which imports all of them. + +The vocabulary uses the prefix `prod:` for `http://ld.company.org/prod-vocab/`. +Run the queries in the Query module. + +!!! tip "No inference" + + The data types products as `prod:Hardware` or `prod:Service`, and the vocabulary declares both as subclasses of `prod:Product`. + The triple store does not infer `prod:Product` from that, so `?product a prod:Product` finds nothing. + The property path `a/rdfs:subClassOf*` follows the class hierarchy in the query itself, and the solutions below use it wherever a superclass is meant. ??? example "1. Find all products" @@ -232,97 +241,138 @@ Run the queries in the Query module, against the graph that contains the instanc SELECT ?product ?label WHERE { - ?product a prod:Product . + ?product a/rdfs:subClassOf* prod:Product . OPTIONAL { ?product rdfs:label ?label . } } ORDER BY ?label ``` -??? example "2. Find products lighter than 10" + The result has 1009 rows: 1000 hardware products and 9 services. + +??? example "2. Find hardware products lighter than 10 g" ```sparql PREFIX prod: + PREFIX rdfs: - SELECT ?product ?weight + SELECT ?product ?label ?weight WHERE { - ?product a prod:Product ; - prod:weight ?weight . + ?product a prod:Hardware ; + rdfs:label ?label ; + prod:weight_g ?weight . FILTER (?weight < 10) } + ORDER BY ?weight ?label ``` - The products vocabulary defines no weight property. - Use the property that the mapping of the `Weigth` column of `products.xlsx` writes to, `prod:weight` is an example. + `prod:weight_g` holds the weight in grams. + 478 of the 1000 hardware products weigh less than 10 g. + +??? example "3. Find products whose product manager is not in the organization map" -??? example "3. Find products and services without a product manager" + Every product in the data has a product manager, so a query for products *without* one returns no rows. + A more revealing question is whether the product manager is a known employee: ```sparql PREFIX prod: + PREFIX rdfs: - SELECT ?item + SELECT ?item ?label ?manager WHERE { - { ?item a prod:Product . } UNION { ?item a prod:Service . } - FILTER NOT EXISTS { ?item prod:hasProductManager ?manager . } + ?item a/rdfs:subClassOf* prod:Product ; + rdfs:label ?label ; + prod:hasProductManager ?manager . + FILTER NOT EXISTS { ?manager a/rdfs:subClassOf* prod:Employee . } } + ORDER BY ?manager ?label ``` + 49 products refer to 3 product managers that the organization map does not contain. + Without the property path, managers typed only as `prod:Manager` would show up as well. + ??? example "4. Find products without a responsible department" ```sparql PREFIX prod: + PREFIX rdfs: - SELECT ?product + SELECT ?item ?label WHERE { - ?product a prod:Product . - FILTER NOT EXISTS { ?department prod:responsibleFor ?product . } + ?item a/rdfs:subClassOf* prod:Product ; + rdfs:label ?label . + FILTER NOT EXISTS { ?department prod:responsibleFor ?item . } } + ORDER BY ?label ``` -??? example "5. Find experts for the category of a product" + Departments are responsible for 58 products, so 951 products have no responsible department. + +??? example "5. Find alternative experts for a product" + + An employee's `prod:areaOfExpertise` names product categories. + Employees who share a category with the product manager can take over responsibility: ```sparql PREFIX prod: + PREFIX rdfs: - SELECT ?product ?category ?expert + SELECT ?productLabel ?managerLabel + (GROUP_CONCAT(DISTINCT ?alternativeLabel; separator=", ") AS ?alternatives) WHERE { - ?product prod:hasCategory ?category . - ?expert prod:expertFor ?category . + VALUES ?product { } + ?product rdfs:label ?productLabel ; + prod:hasProductManager ?manager . + ?manager rdfs:label ?managerLabel ; + prod:areaOfExpertise ?category . + ?alternative prod:areaOfExpertise ?category ; + rdfs:label ?alternativeLabel . + FILTER (?alternative != ?manager) } + GROUP BY ?productLabel ?managerLabel ``` + For the Dipole Transformer, managed by Ratt Hartmann, the query lists 14 colleagues. + Remove the `VALUES` line to get the alternatives for every product. + ??? example "6. Count the products per manager and department" ```sparql PREFIX prod: + PREFIX rdfs: - SELECT ?department ?manager (COUNT(DISTINCT ?product) AS ?products) + SELECT ?departmentLabel ?managerLabel (COUNT(DISTINCT ?product) AS ?products) WHERE { ?product prod:hasProductManager ?manager . - OPTIONAL { ?manager prod:memberOf ?department . } + ?manager rdfs:label ?managerLabel ; + prod:memberOf ?department . + ?department rdfs:label ?departmentLabel . } - GROUP BY ?department ?manager - ORDER BY ?department DESC(?products) + GROUP BY ?departmentLabel ?managerLabel + ORDER BY ?departmentLabel DESC(?products) ``` Save this query in the query catalog. The charts and custom API exercises reuse it. -??? example "7. Insert yourself as an employee" +??? example "7. Add an employee" ```sparql PREFIX prod: PREFIX rdfs: INSERT DATA { - GRAPH { - a prod:Employee ; + GRAPH { + a prod:Employee ; rdfs:label "Jane Doe" ; - prod:email "jane.doe@company.org" . + prod:email "Jane.Doe@company.org" ; + prod:memberOf . } } ``` - Replace the IRI, label and email address with personal values, and the graph IRI with the instance graph of the project. + Replace the name and email address with personal values. + The statements go into a graph of their own, which leaves the package data untouched and is removed again with `DROP GRAPH `. + Because the new employee links to an existing department, the employee appears in the department's data in queries over all graphs. ## Summary { #summary } diff --git a/docs/training/sparql-basics/slides.html b/docs/training/sparql-basics/slides.html index 29f608c78..36885f179 100644 --- a/docs/training/sparql-basics/slides.html +++ b/docs/training/sparql-basics/slides.html @@ -419,8 +419,9 @@ PREFIX rdfs: INSERT DATA { - GRAPH { - a prod:Employee ; + GRAPH { + + a prod:Employee ; rdfs:label "Jane Doe" . } } @@ -463,7 +464,7 @@ # Hands-on -Query the product Knowledge Graph from module 1 +Query the Product Data Demo @@ -471,13 +472,13 @@ diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 86a02e21b..180a153e5 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -1,7 +1,5 @@ --- icon: material/list-status -hide: - - navigation --- # Tutorials diff --git a/nav.yml b/nav.yml index 40a8b8ab8..538b06e24 100644 --- a/nav.yml +++ b/nav.yml @@ -952,11 +952,16 @@ nav: - '2019': - v19.10: - release-notes/2019/corporate-memory-19-10/index.md - - Tutorials: - - tutorials/index.md - - Training: - - Overview: training/index.md - - RDF and Linked Data: - - training/rdf-and-linked-data/index.md - - SPARQL Basics: - - training/sparql-basics/index.md + - Learn: + - Tutorials: + - tutorials/index.md + - Training: + - Overview: training/index.md + - RDF and Linked Data: + - training/rdf-and-linked-data/index.md + - SPARQL Basics: + - training/sparql-basics/index.md + - Ontology Modeling: + - training/ontology-modeling/index.md + - Ontology Engineering: + - training/ontology-engineering/index.md