diff --git a/docs/_template.html b/docs/_template.html index 5a6b9c3e..87717c8f 100644 --- a/docs/_template.html +++ b/docs/_template.html @@ -25,6 +25,16 @@ + + + + {{fsdocs-head-extra}} {{fsdocs-watch-script}} diff --git a/docs/content/fsdocs-default.css b/docs/content/fsdocs-default.css index 7dea2286..f811770e 100644 --- a/docs/content/fsdocs-default.css +++ b/docs/content/fsdocs-default.css @@ -122,6 +122,7 @@ --code-preprocessor-color: #af75c1; --code-fsioutput-color: #808080; --code-tooltip-color: #d1d1d1; + } /* dark theme */ @@ -671,7 +672,10 @@ a { p { line-height: 1.8; - margin-top: var(--spacing-300); + + &:not(:last-child) { + margin-bottom: var(--spacing-300); + } } ol, ul { @@ -713,15 +717,12 @@ blockquote { /* Code snippets */ /* reset browser style */ -pre { - margin: 0; - padding: 0; -} + code, table.pre, pre { background-color: var(--code-background); color: var(--code-color); - font-family: var(--monospace-font); + font-family: var(--monospace-font); font-variant-ligatures: none; font-size: var(--font-200); -webkit-text-size-adjust: 100%; @@ -729,7 +730,7 @@ code, table.pre, pre { h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { font-size: inherit; -} +} table.pre, #content > pre.fssnip { border: 1px solid var(--code-fence-border-color); @@ -1062,20 +1063,11 @@ span[onmouseout] { flex-grow: 1; } - & pre { - margin-bottom: var(--spacing-200); - padding: var(--spacing-50); - flex-grow: 1; - overflow-x: auto; - } } } .fsdocs-summary-contents { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; + width: 100%; } .fsdocs-member-xmldoc-column { @@ -1286,3 +1278,118 @@ dialog { opacity: 0.75; } } + + + + + +/* Code */ + +.fsdocs-api-code { + font-family: var(--monospace-font); + margin-bottom: 1rem; + + a.record-field-name, + a.union-case-property, + a.property { + /* color: darken($primary, 4%); */ + + &:hover { + text-decoration: underline; + } + } + + .property[id]:target, + a[id]:target { + animation-name: blink; + animation-direction: normal; + animation-duration: 0.75s; + animation-iteration-count: 2; + animation-timing-function: ease; + } + + span { + &.property { + color: var(--code-property-color); + } + + &.keyword { + color: var(--code-keywords-color); + } + + &.type { + color: var(--code-reference-color); + } + } + +} + +/* + Animations for blinking the target of a link + + It makes it easier to see where the link is pointing to +*/ +@keyframes blink { + 0% { + background-color: var(--primary); + color: var(--background); + } + 100% { + background-color: transparent; + color: var(--link-color); + } +} + +.docs-example:not(:last-child) { + margin-bottom: var(--spacing-300); +} + +/* + If there is parameter right after this one, add some margin. + + We don't want to do it for all of them, because the last one is followed by a `hr` which already adds spacing around it. +*/ +.fsdocs-doc-parameter { + margin-bottom: var(--spacing-300); +} + +/* +.button { + padding: .5rem .75rem; + color: var(--text-color); + cursor: pointer; + border-radius: var(--radius); + + &:has(iconify-icon) { + display: flex; + justify-content: center; + align-items: center; + } + + &:hover { + background-color: var(--shadow-color); + } +} */ + +.fsdocs-block { + position: relative; + padding: 1rem; + + &:not(:first-child) { + border-top: 1px solid var(--shadow-color); + } + + .actions-buttons { + /* Buttons are in the top right of the current block */ + position: absolute; + top: 1rem; + right: 1rem; + + /* Should be extract as an independant .buttons class when reworking the CSS */ + display: flex; + gap: .25rem; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + } +} \ No newline at end of file diff --git a/docs/content/prism/prism.css b/docs/content/prism/prism.css new file mode 100644 index 00000000..de2dddfa --- /dev/null +++ b/docs/content/prism/prism.css @@ -0,0 +1,507 @@ +/** + * One Light theme for prism.js + * Based on Atom's One Light theme: https://github.com/atom/atom/tree/master/packages/one-light-syntax + */ + +/** + * One Light colours (accurate as of commit eb064bf on 19 Feb 2021) + * From colors.less + * --mono-1: hsl(230, 8%, 24%); + * --mono-2: hsl(230, 6%, 44%); + * --mono-3: hsl(230, 4%, 64%) + * --hue-1: hsl(198, 99%, 37%); + * --hue-2: hsl(221, 87%, 60%); + * --hue-3: hsl(301, 63%, 40%); + * --hue-4: hsl(119, 34%, 47%); + * --hue-5: hsl(5, 74%, 59%); + * --hue-5-2: hsl(344, 84%, 43%); + * --hue-6: hsl(35, 99%, 36%); + * --hue-6-2: hsl(35, 99%, 40%); + * --syntax-fg: hsl(230, 8%, 24%); + * --syntax-bg: hsl(230, 1%, 98%); + * --syntax-gutter: hsl(230, 1%, 62%); + * --syntax-guide: hsla(230, 8%, 24%, 0.2); + * --syntax-accent: hsl(230, 100%, 66%); + * From syntax-variables.less + * --syntax-selection-color: hsl(230, 1%, 90%); + * --syntax-gutter-background-color-selected: hsl(230, 1%, 90%); + * --syntax-cursor-line: hsla(230, 8%, 24%, 0.05); + */ + +code[class*="language-"], +pre[class*="language-"] { + background: hsl(230, 1%, 98%); + color: hsl(230, 8%, 24%); + font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", + monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Selection */ +code[class*="language-"]::-moz-selection, +code[class*="language-"] *::-moz-selection, +pre[class*="language-"] *::-moz-selection { + background: hsl(230, 1%, 90%); + color: inherit; +} + +code[class*="language-"]::selection, +code[class*="language-"] *::selection, +pre[class*="language-"] *::selection { + background: hsl(230, 1%, 90%); + color: inherit; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; + border: 1px solid hsl(230, 1%, 90%); + box-shadow: 5px 5px 0px 1px hsl(230, 1%, 90%);; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: 0.2em 0.3em; + border-radius: 0.3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.cdata { + color: hsl(230, 4%, 64%); +} + +.token.doctype, +.token.punctuation, +.token.entity { + color: hsl(230, 8%, 24%); +} + +.token.attr-name, +.token.class-name, +.token.boolean, +.token.constant, +.token.number, +.token.atrule { + color: hsl(35, 99%, 36%); +} + +.token.keyword { + color: hsl(301, 63%, 40%); +} + +.token.property, +.token.tag, +.token.symbol, +.token.deleted, +.token.important { + color: hsl(5, 74%, 59%); +} + +.token.selector, +.token.string, +.token.char, +.token.builtin, +.token.inserted, +.token.regex, +.token.attr-value, +.token.attr-value > .token.punctuation { + color: hsl(119, 34%, 47%); +} + +.token.variable, +.token.operator, +.token.function { + color: hsl(221, 87%, 60%); +} + +.token.url { + color: hsl(198, 99%, 37%); +} + +/* HTML overrides */ +.token.attr-value > .token.punctuation.attr-equals, +.token.special-attr > .token.attr-value > .token.value.css { + color: hsl(230, 8%, 24%); +} + +/* CSS overrides */ +.language-css .token.selector { + color: hsl(5, 74%, 59%); +} + +.language-css .token.property { + color: hsl(230, 8%, 24%); +} + +.language-css .token.function, +.language-css .token.url > .token.function { + color: hsl(198, 99%, 37%); +} + +.language-css .token.url > .token.string.url { + color: hsl(119, 34%, 47%); +} + +.language-css .token.important, +.language-css .token.atrule .token.rule { + color: hsl(301, 63%, 40%); +} + +/* JS overrides */ +.language-javascript .token.operator { + color: hsl(301, 63%, 40%); +} + +.language-javascript + .token.template-string + > .token.interpolation + > .token.interpolation-punctuation.punctuation { + color: hsl(344, 84%, 43%); +} + +/* JSON overrides */ +.language-json .token.operator { + color: hsl(230, 8%, 24%); +} + +.language-json .token.null.keyword { + color: hsl(35, 99%, 36%); +} + +/* MD overrides */ +.language-markdown .token.url, +.language-markdown .token.url > .token.operator, +.language-markdown .token.url-reference.url > .token.string { + color: hsl(230, 8%, 24%); +} + +.language-markdown .token.url > .token.content { + color: hsl(221, 87%, 60%); +} + +.language-markdown .token.url > .token.url, +.language-markdown .token.url-reference.url { + color: hsl(198, 99%, 37%); +} + +.language-markdown .token.blockquote.punctuation, +.language-markdown .token.hr.punctuation { + color: hsl(230, 4%, 64%); + font-style: italic; +} + +.language-markdown .token.code-snippet { + color: hsl(119, 34%, 47%); +} + +.language-markdown .token.bold .token.content { + color: hsl(35, 99%, 36%); +} + +.language-markdown .token.italic .token.content { + color: hsl(301, 63%, 40%); +} + +.language-markdown .token.strike .token.content, +.language-markdown .token.strike .token.punctuation, +.language-markdown .token.list.punctuation, +.language-markdown .token.title.important > .token.punctuation { + color: hsl(5, 74%, 59%); +} + +/* General */ +.token.bold { + font-weight: bold; +} + +.token.comment, +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.namespace { + opacity: 0.8; +} + +/* Plugin overrides */ +/* Selectors should have higher specificity than those in the plugins' default stylesheets */ + +/* Show Invisibles plugin overrides */ +.token.token.tab:not(:empty):before, +.token.token.cr:before, +.token.token.lf:before, +.token.token.space:before { + color: hsla(230, 8%, 24%, 0.2); +} + +/* Toolbar plugin overrides */ +/* Space out all buttons and move them away from the right edge of the code block */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item { + &:not(:last-child) { + margin-right: 0.5rem; + } +} + +/* Styling the buttons */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { + background: hsl(230, 1%, 90%); + color: hsl(230, 6%, 44%); + padding: 0.5rem; + border-radius: 0.3rem; +} + +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { + background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */ + color: hsl(230, 8%, 24%); + cursor: pointer; +} + +/* Line Highlight plugin overrides */ +/* The highlighted line itself */ +.line-highlight.line-highlight { + background: hsla(230, 8%, 24%, 0.05); +} + +/* Default line numbers in Line Highlight plugin */ +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background: hsl(230, 1%, 90%); + color: hsl(230, 8%, 24%); + padding: 0.1em 0.6em; + border-radius: 0.3em; + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ +} + +/* Hovering over a linkable line number (in the gutter area) */ +/* Requires Line Numbers plugin as well */ +pre[id].linkable-line-numbers.linkable-line-numbers + span.line-numbers-rows + > span:hover:before { + background-color: hsla(230, 8%, 24%, 0.05); +} + +/* Line Numbers and Command Line plugins overrides */ +/* Line separating gutter from coding area */ +.line-numbers.line-numbers .line-numbers-rows, +.command-line .command-line-prompt { + border-right-color: hsla(230, 8%, 24%, 0.2); +} + +/* Stuff in the gutter */ +.line-numbers .line-numbers-rows > span:before, +.command-line .command-line-prompt > span:before { + color: hsl(230, 1%, 62%); +} + +/* Match Braces plugin overrides */ +/* Note: Outline colour is inherited from the braces */ +.rainbow-braces .token.token.punctuation.brace-level-1, +.rainbow-braces .token.token.punctuation.brace-level-5, +.rainbow-braces .token.token.punctuation.brace-level-9 { + color: hsl(5, 74%, 59%); +} + +.rainbow-braces .token.token.punctuation.brace-level-2, +.rainbow-braces .token.token.punctuation.brace-level-6, +.rainbow-braces .token.token.punctuation.brace-level-10 { + color: hsl(119, 34%, 47%); +} + +.rainbow-braces .token.token.punctuation.brace-level-3, +.rainbow-braces .token.token.punctuation.brace-level-7, +.rainbow-braces .token.token.punctuation.brace-level-11 { + color: hsl(221, 87%, 60%); +} + +.rainbow-braces .token.token.punctuation.brace-level-4, +.rainbow-braces .token.token.punctuation.brace-level-8, +.rainbow-braces .token.token.punctuation.brace-level-12 { + color: hsl(301, 63%, 40%); +} + +/* Diff Highlight plugin overrides */ +/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ +pre.diff-highlight > code .token.token.deleted:not(.prefix), +pre > code.diff-highlight .token.token.deleted:not(.prefix) { + background-color: hsla(353, 100%, 66%, 0.15); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix), +pre > code.diff-highlight .token.token.inserted:not(.prefix) { + background-color: hsla(137, 100%, 55%, 0.15); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +/* Previewers plugin overrides */ +/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */ +/* Border around popup */ +.prism-previewer.prism-previewer:before, +.prism-previewer-gradient.prism-previewer-gradient div { + border-color: hsl(0, 0, 95%); +} + +/* Angle and time should remain as circles and are hence not included */ +.prism-previewer-color.prism-previewer-color:before, +.prism-previewer-gradient.prism-previewer-gradient div, +.prism-previewer-easing.prism-previewer-easing:before { + border-radius: 0.3em; +} + +/* Triangles pointing to the code */ +.prism-previewer.prism-previewer:after { + border-top-color: hsl(0, 0, 95%); +} + +.prism-previewer-flipped.prism-previewer-flipped.after { + border-bottom-color: hsl(0, 0, 95%); +} + +/* Background colour within the popup */ +.prism-previewer-angle.prism-previewer-angle:before, +.prism-previewer-time.prism-previewer-time:before, +.prism-previewer-easing.prism-previewer-easing { + background: hsl(0, 0%, 100%); +} + +/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ +/* For time, this is the alternate colour */ +.prism-previewer-angle.prism-previewer-angle circle, +.prism-previewer-time.prism-previewer-time circle { + stroke: hsl(230, 8%, 24%); + stroke-opacity: 1; +} + +/* Stroke colours of the handle, direction point, and vector itself */ +.prism-previewer-easing.prism-previewer-easing circle, +.prism-previewer-easing.prism-previewer-easing path, +.prism-previewer-easing.prism-previewer-easing line { + stroke: hsl(230, 8%, 24%); +} + +/* Fill colour of the handle */ +.prism-previewer-easing.prism-previewer-easing circle { + fill: transparent; +} + +/* Prism toolbar */ + +div.code-toolbar { + position: relative; +} + +div.code-toolbar > .toolbar { + position: absolute; + z-index: 10; + top: 1em; + right: 1em; + transition: opacity 0.3s ease-in-out; + opacity: 0; +} + +div.code-toolbar:hover > .toolbar { + opacity: 1; +} + +/* Separate line b/c rules are thrown out if selector is invalid. + IE11 and old Edge versions don't support :focus-within. */ +div.code-toolbar:focus-within > .toolbar { + opacity: 1; +} + +div.code-toolbar > .toolbar > .toolbar-item { + display: inline-block; +} + +div.code-toolbar > .toolbar > .toolbar-item > a { + cursor: pointer; +} + +div.code-toolbar > .toolbar > .toolbar-item > button { + background: none; + border: 0; + color: inherit; + font: inherit; + line-height: normal; + overflow: visible; + padding: 0; + -webkit-user-select: none; /* for button */ + -moz-user-select: none; + -ms-user-select: none; +} + +div.code-toolbar > .toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar > .toolbar-item > span { + color: #bbb; + font-size: .8em; + padding: 0 .5em; + background: #f5f2f0; + background: rgba(224, 224, 224, 0.2); + box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); + border-radius: .5em; +} + +div.code-toolbar > .toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar > .toolbar-item > span:focus { + color: inherit; + text-decoration: none; +} diff --git a/docs/content/prism/prism.js b/docs/content/prism/prism.js new file mode 100644 index 00000000..3809d6ee --- /dev/null +++ b/docs/content/prism/prism.js @@ -0,0 +1,16 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+dart+fsharp+python+rust&plugins=line-highlight+line-numbers+custom-class+toolbar+copy-to-clipboard */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; +!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; +!function(e){var a=[/\b(?:async|sync|yield)\*/,/\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/],n="(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*",s={pattern:RegExp(n+"[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}}}};e.languages.dart=e.languages.extend("clike",{"class-name":[s,{pattern:RegExp(n+"[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()])"),lookbehind:!0,inside:s.inside}],keyword:a,operator:/\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*\/%&^|=!<>]=?|\?/}),e.languages.insertBefore("dart","string",{"string-literal":{pattern:/r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,lookbehind:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:e.languages.dart}}},string:/[\s\S]+/}},string:void 0}),e.languages.insertBefore("dart","class-name",{metadata:{pattern:/@\w+/,alias:"function"}}),e.languages.insertBefore("dart","class-name",{generics:{pattern:/<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/,inside:{"class-name":s,keyword:a,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism); +Prism.languages.fsharp=Prism.languages.extend("clike",{comment:[{pattern:/(^|[^\\])\(\*(?!\))[\s\S]*?\*\)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(?:"""[\s\S]*?"""|@"(?:""|[^"])*"|"(?:\\[\s\S]|[^\\"])*")B?/,greedy:!0},"class-name":{pattern:/(\b(?:exception|inherit|interface|new|of|type)\s+|\w\s*:\s*|\s:\??>\s*)[.\w]+\b(?:\s*(?:->|\*)\s*[.\w]+\b)*(?!\s*[:.])/,lookbehind:!0,inside:{operator:/->|\*/,punctuation:/\./}},keyword:/\b(?:let|return|use|yield)(?:!\B|\b)|\b(?:abstract|and|as|asr|assert|atomic|base|begin|break|checked|class|component|const|constraint|constructor|continue|default|delegate|do|done|downcast|downto|eager|elif|else|end|event|exception|extern|external|false|finally|fixed|for|fun|function|functor|global|if|in|include|inherit|inline|interface|internal|land|lazy|lor|lsl|lsr|lxor|match|member|method|mixin|mod|module|mutable|namespace|new|not|null|object|of|open|or|override|parallel|private|process|protected|public|pure|rec|sealed|select|sig|static|struct|tailcall|then|to|trait|true|try|type|upcast|val|virtual|void|volatile|when|while|with)\b/,number:[/\b0x[\da-fA-F]+(?:LF|lf|un)?\b/,/\b0b[01]+(?:uy|y)?\b/,/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[fm]|e[+-]?\d+)?\b/i,/\b\d+(?:[IlLsy]|UL|u[lsy]?)?\b/],operator:/([<>~&^])\1\1|([*.:<>&])\2|<-|->|[!=:]=|?|\??(?:<=|>=|<>|[-+*/%=<>])\??|[!?^&]|~[+~-]|:>|:\?>?/}),Prism.languages.insertBefore("fsharp","keyword",{preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(^#)\b(?:else|endif|if|light|line|nowarn)\b/,lookbehind:!0,alias:"keyword"}}}}),Prism.languages.insertBefore("fsharp","punctuation",{"computation-expression":{pattern:/\b[_a-z]\w*(?=\s*\{)/i,alias:"keyword"}}),Prism.languages.insertBefore("fsharp","string",{annotation:{pattern:/\[<.+?>\]/,greedy:!0,inside:{punctuation:/^\[<|>\]$/,"class-name":{pattern:/^\w+$|(^|;\s*)[A-Z]\w*(?=\()/,lookbehind:!0},"annotation-content":{pattern:/[\s\S]+/,inside:Prism.languages.fsharp}}},char:{pattern:/'(?:[^\\']|\\(?:.|\d{3}|x[a-fA-F\d]{2}|u[a-fA-F\d]{4}|U[a-fA-F\d]{8}))'B?/,greedy:!0}}); +Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; +!function(e){for(var a="/\\*(?:[^*/]|\\*(?!/)|/(?!\\*)|)*\\*/",t=0;t<2;t++)a=a.replace(//g,(function(){return a}));a=a.replace(//g,(function(){return"[^\\s\\S]"})),e.languages.rust={comment:[{pattern:RegExp("(^|[^\\\\])"+a),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?"(?:\\[\s\S]|[^\\"])*"|b?r(#*)"(?:[^"]|"(?!\1))*"\1/,greedy:!0},char:{pattern:/b?'(?:\\(?:x[0-7][\da-fA-F]|u\{(?:[\da-fA-F]_*){1,6}\}|.)|[^\\\r\n\t'])'/,greedy:!0},attribute:{pattern:/#!?\[(?:[^\[\]"]|"(?:\\[\s\S]|[^\\"])*")*\]/,greedy:!0,alias:"attr-name",inside:{string:null}},"closure-params":{pattern:/([=(,:]\s*|\bmove\s*)\|[^|]*\||\|[^|]*\|(?=\s*(?:\{|->))/,lookbehind:!0,greedy:!0,inside:{"closure-punctuation":{pattern:/^\||\|$/,alias:"punctuation"},rest:null}},"lifetime-annotation":{pattern:/'\w+/,alias:"symbol"},"fragment-specifier":{pattern:/(\$\w+:)[a-z]+/,lookbehind:!0,alias:"punctuation"},variable:/\$\w+/,"function-definition":{pattern:/(\bfn\s+)\w+/,lookbehind:!0,alias:"function"},"type-definition":{pattern:/(\b(?:enum|struct|trait|type|union)\s+)\w+/,lookbehind:!0,alias:"class-name"},"module-declaration":[{pattern:/(\b(?:crate|mod)\s+)[a-z][a-z_\d]*/,lookbehind:!0,alias:"namespace"},{pattern:/(\b(?:crate|self|super)\s*)::\s*[a-z][a-z_\d]*\b(?:\s*::(?:\s*[a-z][a-z_\d]*\s*::)*)?/,lookbehind:!0,alias:"namespace",inside:{punctuation:/::/}}],keyword:[/\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,/\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\b/],function:/\b[a-z_]\w*(?=\s*(?:::\s*<|\())/,macro:{pattern:/\b\w+!/,alias:"property"},constant:/\b[A-Z_][A-Z_\d]+\b/,"class-name":/\b[A-Z]\w*\b/,namespace:{pattern:/(?:\b[a-z][a-z_\d]*\s*::\s*)*\b[a-z][a-z_\d]*\s*::(?!\s*<)/,inside:{punctuation:/::/}},number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\b/,boolean:/\b(?:false|true)\b/,punctuation:/->|\.\.=|\.{1,3}|::|[{}[\];(),:]/,operator:/[-+*\/%!^]=?|=[=>]?|&[&=]?|\|[|=]?|<>?=?|[@?]/},e.languages.rust["closure-params"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(Prism); +!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document&&document.querySelector){var e,t="line-numbers",i="linkable-line-numbers",n=/\n(?!$)/g,r=!0;Prism.plugins.lineHighlight={highlightLines:function(o,u,c){var h=(u="string"==typeof u?u:o.getAttribute("data-line")||"").replace(/\s+/g,"").split(",").filter(Boolean),d=+o.getAttribute("data-line-offset")||0,f=(function(){if(void 0===e){var t=document.createElement("div");t.style.fontSize="13px",t.style.lineHeight="1.5",t.style.padding="0",t.style.border="0",t.innerHTML=" 
 ",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}()?parseInt:parseFloat)(getComputedStyle(o).lineHeight),p=Prism.util.isActive(o,t),g=o.querySelector("code"),m=p?o:g||o,v=[],y=g.textContent.match(n),b=y?y.length+1:1,A=g&&m!=g?function(e,t){var i=getComputedStyle(e),n=getComputedStyle(t);function r(e){return+e.substr(0,e.length-2)}return t.offsetTop+r(n.borderTopWidth)+r(n.paddingTop)-r(i.paddingTop)}(o,g):0;h.forEach((function(e){var t=e.split("-"),i=+t[0],n=+t[1]||i;if(!((n=Math.min(b+d,n))i&&r.setAttribute("data-end",String(n)),r.style.top=(i-d-1)*f+A+"px",r.textContent=new Array(n-i+2).join(" \n")}));v.push((function(){r.style.width=o.scrollWidth+"px"})),v.push((function(){m.appendChild(r)}))}}));var P=o.id;if(p&&Prism.util.isActive(o,i)&&P){l(o,i)||v.push((function(){o.classList.add(i)}));var E=parseInt(o.getAttribute("data-start")||"1");s(".line-numbers-rows > span",o).forEach((function(e,t){var i=t+E;e.onclick=function(){var e=P+"."+i;r=!1,location.hash=e,setTimeout((function(){r=!0}),1)}}))}return function(){v.forEach(a)}}};var o=0;Prism.hooks.add("before-sanity-check",(function(e){var t=e.element.parentElement;if(u(t)){var i=0;s(".line-highlight",t).forEach((function(e){i+=e.textContent.length,e.parentNode.removeChild(e)})),i&&/^(?: \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}})),Prism.hooks.add("complete",(function e(i){var n=i.element.parentElement;if(u(n)){clearTimeout(o);var r=Prism.plugins.lineNumbers,s=i.plugins&&i.plugins.lineNumbers;l(n,t)&&r&&!s?Prism.hooks.add("line-numbers",e):(Prism.plugins.lineHighlight.highlightLines(n)(),o=setTimeout(c,1))}})),window.addEventListener("hashchange",c),window.addEventListener("resize",(function(){s("pre").filter(u).map((function(e){return Prism.plugins.lineHighlight.highlightLines(e)})).forEach(a)}))}function s(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function l(e,t){return e.classList.contains(t)}function a(e){e()}function u(e){return!!(e&&/pre/i.test(e.nodeName)&&(e.hasAttribute("data-line")||e.id&&Prism.util.isActive(e,i)))}function c(){var e=location.hash.slice(1);s(".temporary.line-highlight").forEach((function(e){e.parentNode.removeChild(e)}));var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var i=e.slice(0,e.lastIndexOf(".")),n=document.getElementById(i);n&&(n.hasAttribute("data-line")||n.setAttribute("data-line",""),Prism.plugins.lineHighlight.highlightLines(n,t,"temporary ")(),r&&document.querySelector(".temporary.line-highlight").scrollIntoView())}}}(); +!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);ts&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;r = "\n" + +let private tagPattern (tagName: string) = + sprintf + """(?'void_element'<%s(?'void_attributes'\s+[^\/>]+)?\/>)|(?'non_void_element'<%s(?'non_void_attributes'\s+[^>]+)?>(?'non_void_innerText'(?:(?!<%s>)(?!<\/%s>)[\s\S])*)<\/%s\s*>)""" + tagName + tagName + tagName + tagName + tagName + +type private TagInfo = + | VoidElement of attributes: Map + | NonVoidElement of innerText: string * attributes: Map + +[] +type private FormatterInfo = + { TagName: string + Formatter: TagInfo -> string option } + +let private extractTextFromQuote (quotedText: string) = + quotedText.Substring(1, quotedText.Length - 2) + +let private extractMemberText (text: string) = + let pattern = "(?'member_type'[a-z]{1}:)?(?'member_text'.*)" + let m = Regex.Match(text, pattern, RegexOptions.IgnoreCase) + + if m.Groups.["member_text"].Success then + m.Groups.["member_text"].Value + else + text + +let private getAttributes (attributes: Group) = + if attributes.Success then + let pattern = """(?'key'\S+)=(?'value''[^']*'|"[^"]*")""" + + Regex.Matches(attributes.Value, pattern, RegexOptions.IgnoreCase) + |> Seq.cast + |> Seq.map (fun m -> m.Groups.["key"].Value, extractTextFromQuote m.Groups.["value"].Value) + |> Map.ofSeq + else + Map.empty + +let rec private applyFormatter (info: FormatterInfo) text = + let pattern = tagPattern info.TagName + + match Regex.Match(text, pattern, RegexOptions.IgnoreCase) with + | m when m.Success -> + if m.Groups.["void_element"].Success then + let attributes = getAttributes m.Groups.["void_attributes"] + + let replacement = VoidElement attributes |> info.Formatter + + match replacement with + | Some replacement -> + text.Replace(m.Groups.["void_element"].Value, replacement) + // Re-apply the formatter, because perhaps there is more + // of the current tag to convert + |> applyFormatter info + + | None -> + // The formatter wasn't able to convert the tag + // Return as it is and don't re-apply the formatter + // otherwise it will create an infinity loop + text + + else if m.Groups.["non_void_element"].Success then + let innerText = m.Groups.["non_void_innerText"].Value + let attributes = getAttributes m.Groups.["non_void_attributes"] + + let replacement = NonVoidElement(innerText, attributes) |> info.Formatter + + match replacement with + | Some replacement -> + // Re-apply the formatter, because perhaps there is more + // of the current tag to convert + text.Replace(m.Groups.["non_void_element"].Value, replacement) + |> applyFormatter info + + | None -> + // The formatter wasn't able to convert the tag + // Return as it is and don't re-apply the formatter + // otherwise it will create an infinity loop + text + else + // Should not happend but like that we are sure to handle all possible cases + text + | _ -> text + +let private codeBlock = + { TagName = "code" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, attributes) -> + let lang = + match Map.tryFind "lang" attributes with + | Some lang -> lang + + | None -> "" + + let formattedText = + if innerText.StartsWith("\n") then + + sprintf "```%s%s```" lang innerText + else + sprintf "```%s\n%s```" lang innerText + + newLine + formattedText + newLine |> Some + + } + |> applyFormatter + +let private codeInline = + { TagName = "c" + Formatter = + function + | VoidElement _ -> None + | NonVoidElement(innerText, _) -> "" + innerText + "" |> Some } + |> applyFormatter + +let private paragraph = + { TagName = "para" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> "

" + innerText + "

" |> Some } + |> applyFormatter + +let private block = + { TagName = "block" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> newLine + innerText + newLine |> Some } + |> applyFormatter + +let private see = + let getCRef (attributes: Map) = Map.tryFind "cref" attributes + + let getHref (attributes: Map) = Map.tryFind "href" attributes + + { TagName = "see" + Formatter = + function + | VoidElement attributes -> + match getCRef attributes with + | Some cref -> + // TODO: Add config to generates command + "" + extractMemberText cref + "" |> Some + + | None -> None + + | NonVoidElement(innerText, attributes) -> + if String.IsNullOrWhiteSpace innerText then + match getCRef attributes with + | Some cref -> + // TODO: Add config to generates command + "" + extractMemberText cref + "" |> Some + + | None -> None + else + match getHref attributes with + | Some href -> sprintf "[%s](%s)" innerText href |> Some + + | None -> "" + innerText + "" |> Some } + |> applyFormatter + +let private paramRef = + let getName (attributes: Map) = Map.tryFind "name" attributes + + { TagName = "paramref" + Formatter = + function + | VoidElement attributes -> + match getName attributes with + | Some name -> "" + name + "" |> Some + + | None -> None + + | NonVoidElement(innerText, attributes) -> None + + } + |> applyFormatter + +let private typeParamRef = + let getName (attributes: Map) = Map.tryFind "name" attributes + + { TagName = "typeparamref" + Formatter = + function + | VoidElement attributes -> + match getName attributes with + | Some name -> "" + name + "" |> Some + + | None -> None + + | NonVoidElement(innerText, attributes) -> None } + |> applyFormatter + +type private Term = string +type private Definition = string + +type private ListStyle = + | Bulleted + | Numbered + | Tablered + +/// ItemList allow a permissive representation of an Item. +/// In theory, TermOnly should not exist but we added it so part of the documentation doesn't disappear +/// TODO: Allow direct text support without and tags +type private ItemList = + /// A list where the items are just contains in a element + | DescriptionOnly of string + /// A list where the items are just contains in a element + | TermOnly of string + /// A list where the items are a term followed by a definition (ie in markdown: * - ) + | Definitions of Term * Definition + +let private itemListToStringAsMarkdownList (item: ItemList) = + match item with + | DescriptionOnly description -> description + | TermOnly term -> "" + term + "" + | Definitions(term, description) -> "" + term + " - " + description + +let private list = + let getType (attributes: Map) = Map.tryFind "type" attributes + + let tryGetInnerTextOnNonVoidElement (text: string) (tagName: string) = + match Regex.Match(text, tagPattern tagName, RegexOptions.IgnoreCase) with + | m when m.Success -> + if m.Groups.["non_void_element"].Success then + Some m.Groups.["non_void_innerText"].Value + else + None + | _ -> None + + let tryGetNonVoidElement (text: string) (tagName: string) = + match Regex.Match(text, tagPattern tagName, RegexOptions.IgnoreCase) with + | m when m.Success -> + if m.Groups.["non_void_element"].Success then + Some(m.Groups.["non_void_element"].Value, m.Groups.["non_void_innerText"].Value) + else + None + | _ -> None + + let tryGetDescription (text: string) = + tryGetInnerTextOnNonVoidElement text "description" + + let tryGetTerm (text: string) = + tryGetInnerTextOnNonVoidElement text "term" + + let rec extractItemList (res: ItemList list) (text: string) = + match Regex.Match(text, tagPattern "item", RegexOptions.IgnoreCase) with + | m when m.Success -> + let newText = text.Substring(m.Value.Length) + + if m.Groups.["non_void_element"].Success then + let innerText = m.Groups.["non_void_innerText"].Value + let description = tryGetDescription innerText + let term = tryGetTerm innerText + + let currentItem: ItemList option = + match description, term with + | Some description, Some term -> Definitions(term, description) |> Some + | Some description, None -> DescriptionOnly description |> Some + | None, Some term -> TermOnly term |> Some + | None, None -> None + + match currentItem with + | Some currentItem -> extractItemList (res @ [ currentItem ]) newText + | None -> extractItemList res newText + else + extractItemList res newText + | _ -> res + + let rec extractColumnHeader (res: string list) (text: string) = + match Regex.Match(text, tagPattern "listheader", RegexOptions.IgnoreCase) with + | m when m.Success -> + let newText = text.Substring(m.Value.Length) + + if m.Groups.["non_void_element"].Success then + let innerText = m.Groups.["non_void_innerText"].Value + + let rec extractAllTerms (res: string list) (text: string) = + match tryGetNonVoidElement text "term" with + | Some(fullString, innerText) -> + let escapedRegex = Regex(Regex.Escape(fullString)) + let newText = escapedRegex.Replace(text, "", 1) + extractAllTerms (res @ [ innerText ]) newText + | None -> res + + extractColumnHeader (extractAllTerms [] innerText) newText + else + extractColumnHeader res newText + | _ -> res + + let rec extractRowsForTable (res: (string list) list) (text: string) = + match Regex.Match(text, tagPattern "item", RegexOptions.IgnoreCase) with + | m when m.Success -> + let newText = text.Substring(m.Value.Length) + + if m.Groups.["non_void_element"].Success then + let innerText = m.Groups.["non_void_innerText"].Value + + let rec extractAllTerms (res: string list) (text: string) = + match tryGetNonVoidElement text "term" with + | Some(fullString, innerText) -> + let escapedRegex = Regex(Regex.Escape(fullString)) + let newText = escapedRegex.Replace(text, "", 1) + extractAllTerms (res @ [ innerText ]) newText + | None -> res + + extractRowsForTable (res @ [ extractAllTerms [] innerText ]) newText + else + extractRowsForTable res newText + | _ -> res + + { TagName = "list" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, attributes) -> + let listStyle = + match getType attributes with + | Some "bullet" -> Bulleted + | Some "number" -> Numbered + | Some "table" -> Tablered + | Some _ + | None -> Bulleted + + match listStyle with + | Bulleted -> + let items = + extractItemList [] innerText + |> List.map (fun item -> "
  • " + itemListToStringAsMarkdownList item + "
  • ") + |> String.concat newLine + + "
      " + newLine + items + newLine + "
    " + + | Numbered -> + let items = + extractItemList [] innerText + |> List.map (fun item -> "
  • " + itemListToStringAsMarkdownList item + "
  • ") + |> String.concat newLine + + "
      " + newLine + items + newLine + "
    " + + | Tablered -> + let columnHeaders = extractColumnHeader [] innerText + let rows = extractRowsForTable [] innerText + + let columnHeadersText = + columnHeaders + |> List.mapi (fun index header -> "" + header + "") + |> String.concat "" + + let itemsText = + rows + |> List.map (fun columns -> + let rowContent = + columns + |> List.mapi (fun index column -> "" + column + "") + |> String.concat newLine + + "" + newLine + rowContent + newLine + "") + |> String.concat newLine + + "" + + newLine + + "" + + newLine + + columnHeadersText + + newLine + + "" + + newLine + + "" + + newLine + + itemsText + + newLine + + "" + + newLine + + "
    " + |> Some } + |> applyFormatter + +/// +/// Unescape XML special characters +/// +/// For example, this allows to print '>' in the tooltip instead of '>' +/// +let private unescapeSpecialCharacters (text: string) = + text + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'") + .Replace("&", "&") + +let private summary = + { TagName = "summary" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> + """
    """ + newLine + innerText + newLine + "
    " + |> Some + + } + |> applyFormatter + +let private example = + { TagName = "example" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> + newLine + + """
    """ + + newLine + + """

    Example

    """ + + newLine + + innerText + + newLine + + "
    " + + newLine + + newLine + |> Some + + } + |> applyFormatter + +let private removeSummaryTag = + { TagName = "summary" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> innerText |> Some + + } + |> applyFormatter + +let private removeParamElement = + { TagName = "param" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(_, _) -> + // Returning an empty string will completely remove the element + Some "" + + } + |> applyFormatter + +let private removeRemarkTag = + { TagName = "remark" + Formatter = + function + | VoidElement _ -> None + + | NonVoidElement(innerText, _) -> innerText |> Some + + } + |> applyFormatter + +/// +/// Format the given doc comments text to HTML +/// +/// +/// +let format (text: string) = + text + |> removeSummaryTag + |> removeParamElement + |> removeRemarkTag + |> example + |> paragraph + |> block + |> codeBlock + |> codeInline // Important: Apply code inline after the codeBlock as we are generating tags + |> see + |> paramRef + |> typeParamRef + |> list + |> unescapeSpecialCharacters + |> Markdown.ToHtml + +/// +/// Extract and format only the summary tag +/// +/// +/// +let formatSummaryOnly (text: string) = + let pattern = tagPattern "summary" + + // Match all the param tags + match Regex.Match(text, pattern, RegexOptions.IgnoreCase) with + | m when m.Success -> + if m.Groups.["void_element"].Success then + "" + else if m.Groups.["non_void_element"].Success then + m.Groups.["non_void_innerText"].Value |> format + + else + // Should not happen but we are forced to handle it by F# compiler + "" + + | _ -> "" + +/// +/// Try to extract a specific param tag and format +/// +/// +/// Return the formatted param tag doc if found. +/// +/// Otherwise, it returns None +/// +let tryFormatParam (parameterName: string) (text: string) = + let pattern = tagPattern "param" + + // Match all the param tags + Regex.Matches(text, pattern, RegexOptions.IgnoreCase) + // Try find the param tag that has name attribute equal to the parameterName + |> Seq.tryFind (fun m -> + if m.Groups.["void_element"].Success then + false + else if m.Groups.["non_void_element"].Success then + let attributes = getAttributes m.Groups.["non_void_attributes"] + + match Map.tryFind "name" attributes with + | Some name -> name = parameterName + + | None -> false + else + // Should not happen but we are forced to handle it by F# compiler + false) + // Extract the inner text of the param tag + |> Option.map (fun m -> m.Groups.["non_void_innerText"].Value |> format) + +let tryFormatReturnsOnly (text: string) = + let pattern = tagPattern "returns" + + match Regex.Match(text, pattern, RegexOptions.IgnoreCase) with + | m when m.Success -> + if m.Groups.["void_element"].Success then + None + else if m.Groups.["non_void_element"].Success then + m.Groups.["non_void_innerText"].Value |> format |> Some + + else + // Should not happen but we are forced to handle it by F# compiler + None + + | _ -> None diff --git a/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj b/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj index 1255480c..b3ae7573 100644 --- a/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj +++ b/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj @@ -10,8 +10,11 @@ Common\StringParsing.fs + + + diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index a6067d23..ab002d80 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -9,6 +9,9 @@ open FSharp.Compiler.Symbols open FSharp.Formatting.Templating open FSharp.Formatting.HtmlModel open FSharp.Formatting.HtmlModel.Html +open System.Xml.Linq +open System.Text.RegularExpressions +open FSharp.Formatting.ApiDocs.GenerateSignature /// Embed some HTML generated in GenerateModel let embed (x: ApiDocHtml) = !!x.HtmlText @@ -20,6 +23,36 @@ let fsdocsSummary (x: ApiDocHtml) = else div [ Class "fsdocs-summary-contents" ] [ p [ Class "fsdocs-summary" ] [ embed x ] ] +let formatXmlComment (commentOpt: XElement option) : string = + + match commentOpt with + | Some comment -> + let docComment = comment.ToString() + + let pattern = $"""((?'xml_doc'(?:(?!)(?!<\/member>)[\s\S])*)<\/member\s*>)""" + + let m = Regex.Match(docComment, pattern) + + // Remove the and tags + if m.Success then + let xmlDoc = m.Groups.["xml_doc"].Value + + let lines = xmlDoc |> String.splitLines |> Array.toList + + // Remove the non meaning full indentation + let content = + lines + |> List.map (fun line -> + // Add a small protection in case the user didn't align all it's tags + if line.StartsWith(" ") then line.Substring(1) else line) + |> String.concat "\n" + + CommentFormatter.format content + else + CommentFormatter.format docComment + + | None -> "" + type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) = let root = model.Root let collectionName = model.Collection.CollectionName @@ -101,6 +134,151 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) = | :? FSharpEntity as v -> copyXmlSigIconMarkdown (removeParen v.XmlDocSig) | _ -> () ] + let renderValueOrFunctions (entities: ApiDocMember list) = + + if entities.IsEmpty then + [] + else + + [ h3 [] [ !! "Functions and values" ] + + for entity in entities do + let (ApiDocMemberDetails(usageHtml, + paramTypes, + returnType, + modifiers, + typars, + baseType, + location, + compiledName)) = + entity.Details + + let returnHtml = + // TODO: Parse the return type information from + // let x = entity.Symbol :?> FSharpMemberOrFunctionOrValue + // x.FullType <-- Here we have access to all the type including the argument for the function that we should ignore... (making the processing complex) + // For now, we are just using returnType.HtmlText to have something ready as parsing from + // FSharpMemberOrFunctionOrValue seems to be quite complex + match returnType with + | Some(_, returnType) -> + // Remove the starting and ending + returnType.HtmlText.[6 .. returnType.HtmlText.Length - 8] + // Adapt the text to have basic syntax highlighting + |> fun text -> text.Replace("<", Html.lessThan.ToMinifiedHtml()) + |> fun text -> text.Replace(">", Html.greaterThan.ToMinifiedHtml()) + |> fun text -> text.Replace(",", Html.comma.ToMinifiedHtml()) + + | None -> "unit" + + let initial = Signature.ParamTypesInformation.Init entity.Name + + let paramTypesInfo = Signature.extractParamTypesInformation initial paramTypes + + div [ Class "fsdocs-block" ] [ + + div [ Class "actions-buttons" ] [ + yield! sourceLink entity.SourceLocation + yield! copyXmlSigIconForSymbol entity.Symbol + yield! copyXmlSigIconForSymbolMarkdown entity.Symbol + ] + + // This is a value + if paramTypesInfo.Infos.IsEmpty then + div [ Class "fsdocs-api-code" ] [ + div [] [ Html.val'; Html.space; !!entity.Name; Html.space; Html.colon; !!returnHtml ] + ] + + // This is a function + else + + div [ Class "fsdocs-api-code" ] [ + [ TextNode.Div [ + TextNode.Keyword "val" + TextNode.Space + TextNode.AnchorWithId($"#{entity.Name}", entity.Name, entity.Name) + TextNode.Space + TextNode.Colon + ] ] + |> TextNode.Node + |> TextNode.ToHtmlElement + + for index in 0 .. paramTypesInfo.Infos.Length - 1 do + let (name, returnType) = paramTypesInfo.Infos.[index] + + div [] [ + Html.spaces 4 // Equivalent to 'val ' + !!name + Html.spaces (paramTypesInfo.MaxNameLength - name.Length + 1) // Complete with space to align ':' + Html.colon + Html.space + !! returnType.HtmlElement.ToMinifiedHtml() + + Html.spaces (paramTypesInfo.MaxReturnTypeLength - returnType.Length + 1) // Complete with space to align '->' + + // Don't add the arrow for the last parameter + if index <> paramTypesInfo.Infos.Length - 1 then + Html.arrow + ] + |> Html.minify + + div [] [ + Html.spaces (4 + paramTypesInfo.MaxNameLength + 1) // Equivalent to 'val ' + the max length of parameter name + ':' + Html.arrow + Html.space + !!returnHtml + ] + |> Html.minify + ] + + match entity.Comment.Xml with + | Some xmlComment -> + let comment = xmlComment.ToString() + !!(CommentFormatter.formatSummaryOnly comment) + + if not paramTypesInfo.Infos.IsEmpty then + p [] [ strong [] [ !! "Parameters" ] ] + + + for (name, returnType) in paramTypesInfo.Infos do + let paramDoc = + CommentFormatter.tryFormatParam name comment + |> Option.map (fun paramDoc -> !!paramDoc) + |> Option.defaultValue Html.nothing + + div [ Class "fsdocs-doc-parameter" ] [ + [ TextNode.DivWithClass( + "fsdocs-api-code", + [ TextNode.Property name + TextNode.Space + TextNode.Colon + TextNode.Space + returnType ] + ) ] + |> TextNode.Node + |> TextNode.ToHtmlElement + + paramDoc + ] + + match CommentFormatter.tryFormatReturnsOnly comment with + | Some returnDoc -> + p [] [ strong [] [ !! "Returns" ] ] + + !!returnDoc + + | None -> () + + // TODO: Should we r``ender a minimal documentation here with the information we have? + // For example, we can render the list of parameters and the return type + // This is to make the documentation more consistent + // However, these minimal information will be rondontant with the information displayed in the signature + | None -> () + ] + + // hr [] + + ] + let renderMembers header tableHeader (members: ApiDocMember list) = [ if members.Length > 0 then h3 [] [ !!header ] @@ -330,7 +508,9 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) = ] td [ Class "fsdocs-entity-xmldoc" ] [ div [] [ - fsdocsSummary e.Comment.Summary + + div [ Class "fsdocs-summary-contents" ] [ !!(formatXmlComment e.Comment.Xml) ] + div [ Class "icon-button-row" ] [ yield! sourceLink e.SourceLocation yield! copyXmlSigIconForSymbol e.Symbol @@ -481,7 +661,7 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) = let constructors = ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.Constructor) let instanceMembers = ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.InstanceMember) let staticMembers = ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.StaticMember) - div [] (renderMembers "Functions and values" "Function or value" functionsOrValues) + div [] (renderValueOrFunctions functionsOrValues) div [] (renderMembers "Type extensions" "Type extension" extensions) div [] (renderMembers "Active patterns" "Active pattern" activePatterns) div [] (renderMembers "Union cases" "Union case" unionCases) diff --git a/src/FSharp.Formatting.ApiDocs/GenerateSignature.fs b/src/FSharp.Formatting.ApiDocs/GenerateSignature.fs new file mode 100644 index 00000000..3d53d818 --- /dev/null +++ b/src/FSharp.Formatting.ApiDocs/GenerateSignature.fs @@ -0,0 +1,320 @@ +module internal FSharp.Formatting.ApiDocs.GenerateSignature + +open System +open System.Collections.Generic +open System.IO +open System.Web +open FSharp.Formatting.Common +open FSharp.Compiler.Symbols +open FSharp.Formatting.Templating +open FSharp.Formatting.HtmlModel +open FSharp.Formatting.HtmlModel.Html +open System.Xml.Linq +open System.Text.RegularExpressions + +/// +/// Type used to represent a text node. +/// +/// This is mostly used to render API signature while also being able to compute the length of the text +/// in term of characters to align the signature. +/// +[] +type TextNode = + | Text of string + | Anchor of url: string * label: string + | AnchorWithId of url: string * id: string * label: string + | Space + | Dot + | Comma + | Arrow + | GreaterThan + | Colon + | LessThan + | LeftParent + | RightParent + | Equal + | Tick + | Node of TextNode list + | Keyword of string + | NewLine + | Spaces of int + | Div of TextNode list + | DivWithClass of string * TextNode list + | Property of string + | Paragraph of TextNode list + + static member ToHtmlElement(node: TextNode) : HtmlElement = node.HtmlElement + + member this.HtmlElement: HtmlElement = + match this with + | Text s -> !!s + | Colon -> Html.colon + | Anchor(url, text) -> a [ Href url ] [ !!text ] + | AnchorWithId(url, id, text) -> a [ Href url; Id id ] [ !!text ] + | Keyword text -> Html.keyword text + | Property text -> Html.property text + | Div nodes -> div [] (nodes |> List.map (fun node -> node.HtmlElement)) + | DivWithClass(cls, nodes) -> div [ Class cls ] (nodes |> List.map (fun node -> node.HtmlElement)) + | Paragraph nodes -> p [] (nodes |> List.map (fun node -> node.HtmlElement)) + | Spaces n -> + [ for _ in 0..n do + Space ] + |> Node + |> TextNode.ToHtmlElement + | NewLine -> !! "\n" // Should it be
    instead? + | Arrow -> Html.arrow + | Dot -> Html.dot + | Comma -> Html.comma + | Space -> Html.space + | GreaterThan -> Html.greaterThan + | LessThan -> Html.lessThan + | Equal -> Html.keyword "=" + | Tick -> !! "'" + | LeftParent -> Html.leftParent + | RightParent -> Html.rightParent + | Node node -> + // TODO: Can we have something similar to fragments in React? + let elements = span [] (node |> List.map (fun node -> node.HtmlElement)) + + !! elements.ToMinifiedHtml() + + member this.Length = + match this with + | NewLine -> 0 + // 1 character + | Comma + | Colon + | Dot + | Space + | GreaterThan + | LessThan + | LeftParent + | RightParent + | Equal + | Tick -> 1 + // 2 characters + | Anchor(_, text) + | AnchorWithId(_, _, text) + | Keyword text + | Property text -> text.Length + | Arrow -> 2 + // X characters + | Text s -> s.Length + | Spaces count -> count + // Sum of children + | Node nodes + | Div nodes + | DivWithClass(_, nodes) + | Paragraph nodes -> nodes |> List.map (fun node -> node.Length) |> List.sum + +[] +module Signature = + + /// + /// Generate a list of generic parameters + /// + /// 'T, 'T2, 'MyType + /// + /// + /// + /// + let renderGenericParameters (parameters: IList) : TextNode = + [ for index in 0 .. parameters.Count - 1 do + let param = parameters.[index] + + if index <> 0 then + TextNode.Comma + TextNode.Space + + TextNode.Tick + TextNode.Text param.DisplayName ] + |> TextNode.Node + + let rec renderParameterType (isTopLevel: bool) (typ: FSharpType) : TextNode = + // This correspond to a generic paramter like: 'T + if typ.IsGenericParameter then + TextNode.Node [ TextNode.Tick; TextNode.Text typ.GenericParameter.DisplayName ] + // Not a generic type we can display it as it is + // Example: + // - string + // - int + // - MyObject + else if typ.GenericArguments.Count = 0 then + TextNode.Text typ.TypeDefinition.DisplayName + + // This is a generic type we need more logic + else if + // This is a function, we need to generate something like: + // - 'T -> string + // - 'T -> 'T option + typ.IsFunctionType + then + let separator = TextNode.Node [ TextNode.Space; TextNode.Arrow; TextNode.Space ] + + let result = + [ for index in 0 .. typ.GenericArguments.Count - 1 do + let arg = typ.GenericArguments.[index] + + // Add the separator if this is not the first argument + if index <> 0 then + separator + + // This correspond to a generic paramter like: 'T + if arg.IsGenericParameter then + TextNode.Tick + TextNode.Text arg.GenericParameter.DisplayName + + // This is a type definition like: 'T option or Choice<'T1, 'T2> + else if arg.HasTypeDefinition then + // For some generic types definition we don't add the generic arguments + if + arg.TypeDefinition.DisplayName = "exn" + || arg.TypeDefinition.DisplayName = "unit" + then + + TextNode.Text arg.TypeDefinition.DisplayName + + else + // This is the name of the type definition + // In Choice<'T1, 'T2> this correspond to Choice + TextNode.Text arg.TypeDefinition.DisplayName + TextNode.LessThan + // Render the generic parameters list in the form of 'T1, 'T2 + renderGenericParameters arg.TypeDefinition.GenericParameters + + TextNode.GreaterThan + + else if arg.IsFunctionType then + + let res = + [ for index in 0 .. arg.GenericArguments.Count - 1 do + let arg = arg.GenericArguments.[index] + + if index <> 0 then + TextNode.Space + TextNode.Arrow + TextNode.Space + + renderParameterType false arg ] + + // Try to detect curried case + // Like in: + // let create (f: ('T -> unit) -> (exn -> unit) -> unit): JS.Promise<'T> = jsNative + // FCS gives back an equivalent of : + // let create (f: ('T -> unit) -> ((exn -> unit) -> unit)): JS.Promise<'T> = jsNative + // So we try to detect it to avoid the extract Parents + match res with + | (TextNode.Node(TextNode.LeftParent :: _) :: _) -> TextNode.Node res + + | _ -> + TextNode.Node + [ TextNode.LeftParent + + yield! res + + TextNode.RightParent ] + + else + TextNode.Text "Unkown syntax please open an issue" ] + + // If this is a top level function we don't neeed to add the parenthesis + TextNode.Node + [ if not isTopLevel then + TextNode.LeftParent + + TextNode.Node result + + if not isTopLevel then + TextNode.RightParent ] + + else + let separator = TextNode.Node [ TextNode.Space; TextNode.Comma ] + + let result = + [ for index in 0 .. typ.GenericArguments.Count - 1 do + let arg = typ.GenericArguments.[index] + + // Add the separator if this is not the first argument + if index <> 0 then + separator + + if arg.IsGenericParameter then + TextNode.Tick + TextNode.Text arg.GenericParameter.DisplayName + else + // TODO: Generate an URL with the version of the package + + let url = + // FIXME: This is a temporary fix to avoid the error + try + arg.TypeDefinition.FullName + |> String.toLower + |> String.replace "." "-" + |> String.append ".html" + with _ -> + "" + + let subType = renderParameterType false arg + + TextNode.Anchor(url, arg.TypeDefinition.DisplayName) + TextNode.LessThan + + subType + + TextNode.GreaterThan ] + + TextNode.Node result + + type ParamTypesInformation = + { Infos: (string * TextNode) list + MaxNameLength: int + MaxReturnTypeLength: int } + + static member Init(entityName: string) = + { Infos = [] + MaxNameLength = entityName.Length + MaxReturnTypeLength = 0 } + + /// + /// Extracts parameter types information from a list of parameter types. + /// + /// The goals is to extract the information about the max length of the name and the return type + /// to be able to format the information in a nice way. + /// + /// It will allows us to align the colon, arrows and other symbols. + /// + /// The current state of parameter types information + /// The list of parameter types to extract information from + /// The list of parameters and the max length of the name and return type + let rec extractParamTypesInformation + (state: ParamTypesInformation) + (paramTypes: list * string * ApiDocHtml>) + = + + match paramTypes with + | paramType :: tail -> + match paramType with + | Choice1Of2 fsharpParameter, name, _apiDoc -> + let returnType = renderParameterType true fsharpParameter.Type + + let newState = + { state with + Infos = state.Infos @ [ name, returnType ] + MaxNameLength = System.Math.Max(state.MaxNameLength, name.Length) + MaxReturnTypeLength = System.Math.Max(state.MaxReturnTypeLength, returnType.Length) } + + extractParamTypesInformation newState tail + + // TODO: I didn't encounter this case yet, so I a not sure how to handle it + | Choice2Of2 _fsharpField, _name, _apiDoc -> + let newState = + { state with + Infos = + state.Infos + @ [ "TODO: extractParamTypesInformation -> fsharpField", TextNode.Div [] ] } + + failwith "Not implemented" + + extractParamTypesInformation newState tail + + | [] -> state diff --git a/src/FSharp.Formatting.ApiDocs/Prelude.fs b/src/FSharp.Formatting.ApiDocs/Prelude.fs new file mode 100644 index 00000000..eb404d02 --- /dev/null +++ b/src/FSharp.Formatting.ApiDocs/Prelude.fs @@ -0,0 +1,44 @@ +[] +module internal FSharp.Formatting.ApiDocs.Prelude + +open FSharp.Formatting.HtmlModel +open FSharp.Formatting.HtmlModel.Html + +[] +module Html = + let wrapInClass cls text = span [ Class cls ] [ !!text ] + let keyword text = wrapInClass "keyword" text + let property text = wrapInClass "property" text + + let val' = keyword "val" + let space = !! " " + let spaces count = !!(String.replicate count " ") + let comma = keyword "," + let colon = keyword ":" + let arrow = keyword "->" + let dot = keyword "." + + let greaterThan = keyword ">" + let lessThan = keyword "<" + let nothing = !! "" + let equal = keyword "=" + let leftParent = keyword "(" + let rightParent = keyword ")" + + let minify (html: HtmlElement) = !!(html.ToMinifiedHtml()) + +[] +module String = + + let normalizeEndOfLine (text: string) = text.Replace("\r\n", "\n") + + let splitBy (c: char) (text: string) = text.Split(c) + + let splitLines (text: string) = + text |> normalizeEndOfLine |> splitBy '\n' + + let toLower (text: string) = text.ToLower() + + let replace (oldValue: string) (newValue: string) (text: string) = text.Replace(oldValue, newValue) + + let append (value: string) (text: string) = text + value diff --git a/src/FSharp.Formatting.Common/HtmlModel.fs b/src/FSharp.Formatting.Common/HtmlModel.fs index 17da8ae8..51e6bdd8 100644 --- a/src/FSharp.Formatting.Common/HtmlModel.fs +++ b/src/FSharp.Formatting.Common/HtmlModel.fs @@ -435,6 +435,176 @@ type internal HtmlElement = | EncodeString of string | CustomElement of element: string * props: HtmlProperties list * children: HtmlElement list + // TODO: The way F# Formatting format the HTML is causing issues because it "beautifies" the HTML too much + // which causes issues with the control over the spaces. + // Do we need to have a beautiful HTML generated? + // In theory, for performance reasons, we should the most minified HTML possible. + member tag.ToMinifiedHtml() = + let rec format tag (props: HtmlProperties list) (children: HtmlElement list) = + let cnt = + if children.Length > 0 then + (children + |> List.map (fun n -> (String.replicate 0 " ") + helper n) + |> String.concat "") + else + "" + + let attrs = + if props.Length > 0 then + " " + (props |> List.map string |> String.concat " ") + else + "" + + sprintf "<%s%s>%s" tag attrs cnt tag + + and formatVoid tag (props: HtmlProperties list) = + let attrs = + if props.Length > 0 then + " " + (props |> List.map string |> String.concat " ") + else + "" + + sprintf "<%s%s/>" tag attrs + + and helper tag = + match tag with + | A(props, children) -> format "a" props children + | Abbr(props, children) -> format "abbr" props children + | Address(props, children) -> format "address" props children + | Area(props) -> formatVoid "area" props + | Article(props, children) -> format "article" props children + | Aside(props, children) -> format "aside" props children + | Audio(props, children) -> format "audio" props children + | B(props, children) -> format "b" props children + | Base(props) -> formatVoid "base" props + | Bdi(props, children) -> format "bdi" props children + | Bdo(props, children) -> format "bdo" props children + | Big(props, children) -> format "big" props children + | Blockquote(props, children) -> format "blockquote" props children + | Body(props, children) -> format "body" props children + | Br(props) -> formatVoid "br" props + | Button(props, children) -> format "button" props children + | Canvas(props, children) -> format "canvas" props children + | Caption(props, children) -> format "caption" props children + | Cite(props, children) -> format "cite" props children + | Code(props, children) -> format "code" props children + | Col(props) -> formatVoid "col" props + | Colgroup(props, children) -> format "colgroup" props children + | Data(props, children) -> format "data" props children + | Datalist(props, children) -> format "datalist" props children + | Dd(props, children) -> format "dd" props children + | Del(props, children) -> format "del" props children + | Details(props, children) -> format "details" props children + | Dfn(props, children) -> format "dfn" props children + | Dialog(props, children) -> format "dialog" props children + | Div(props, children) -> format "div" props children + | Dl(props, children) -> format "dl" props children + | Dt(props, children) -> format "dt" props children + | Em(props, children) -> format "em" props children + | Embed(props) -> formatVoid "embed" props + | Fieldset(props, children) -> format "fieldset" props children + | Figcaption(props, children) -> format "figcaption" props children + | Figure(props, children) -> format "figure" props children + | Footer(props, children) -> format "footer" props children + | Form(props, children) -> format "form" props children + | H1(props, children) -> format "h1" props children + | H2(props, children) -> format "h2" props children + | H3(props, children) -> format "h3" props children + | H4(props, children) -> format "h4" props children + | H5(props, children) -> format "h5" props children + | H6(props, children) -> format "h6" props children + | Head(props, children) -> format "head" props children + | Header(props, children) -> format "header" props children + | Hgroup(props, children) -> format "hgroup" props children + | Hr(props) -> formatVoid "hr" props + | Html(props, children) -> format "html" props children + | I(props, children) -> format "i" props children + | Iframe(props, children) -> format "iframe" props children + | Img(props) -> formatVoid "img" props + | Input(props) -> formatVoid "input" props + | Ins(props, children) -> format "ins" props children + | Kbd(props, children) -> format "kbd" props children + | Keygen(props) -> formatVoid "keygen" props + | Label(props, children) -> format "label" props children + | Legend(props, children) -> format "legend" props children + | Li(props, children) -> format "li" props children + | Link(props) -> formatVoid "link" props + | Main(props, children) -> format "main" props children + | Map(props, children) -> format "map" props children + | Mark(props, children) -> format "mark" props children + | Menu(props, children) -> format "menu" props children + | Menuitem(props) -> formatVoid "menuitem" props + | Meta(props) -> formatVoid "meta" props + | Meter(props, children) -> format "meter" props children + | Nav(props, children) -> format "nav" props children + | Noscript(props, children) -> format "noscript" props children + | Object(props, children) -> format "object" props children + | Ol(props, children) -> format "ol" props children + | Optgroup(props, children) -> format "optgroup" props children + | Option(props, children) -> format "option" props children + | Output(props, children) -> format "output" props children + | P(props, children) -> format "p" props children + | Param(props) -> formatVoid "param" props + | Picture(props, children) -> format "picture" props children + | Pre(props, children) -> format "pre" props children + | Progress(props, children) -> format "progress" props children + | Q(props, children) -> format "q" props children + | Rp(props, children) -> format "rp" props children + | Rt(props, children) -> format "rt" props children + | Ruby(props, children) -> format "ruby" props children + | S(props, children) -> format "s" props children + | Samp(props, children) -> format "samp" props children + | Script(props, children) -> format "script" props children + | Section(props, children) -> format "section" props children + | Select(props, children) -> format "select" props children + | Small(props, children) -> format "small" props children + | Source(props) -> formatVoid "source" props + | Span(props, children) -> format "span" props children + | Strong(props, children) -> format "strong" props children + | Style(props, children) -> format "style" props children + | Sub(props, children) -> format "sub" props children + | Summary(props, children) -> format "summary" props children + | Sup(props, children) -> format "sup" props children + | Table(props, children) -> format "table" props children + | Tbody(props, children) -> format "tbody" props children + | Td(props, children) -> format "td" props children + | Textarea(props, children) -> format "textarea" props children + | Tfoot(props, children) -> format "tfoot" props children + | Th(props, children) -> format "th" props children + | Thead(props, children) -> format "thead" props children + | Time(props, children) -> format "time" props children + | Title(props, children) -> format "title" props children + | Tr(props, children) -> format "tr" props children + | Track(props) -> formatVoid "track" props + | U(props, children) -> format "u" props children + | Ul(props, children) -> format "ul" props children + | Var(props, children) -> format "var" props children + | Video(props, children) -> format "video" props children + | Wbr(props) -> formatVoid "wbr" props + | Svg(props, children) -> format "svg" props children + | Circle(props, children) -> format "circle" props children + | Defs(props, children) -> format "defs" props children + | Ellipse(props, children) -> format "ellipse" props children + | G(props, children) -> format "g" props children + | Image(props, children) -> format "image" props children + | Line(props, children) -> format "line" props children + | LinearGradient(props, children) -> format "radient" props children + | Mask(props, children) -> format "mask" props children + | Path(props, children) -> format "path" props children + | Pattern(props, children) -> format "pattern" props children + | Polygon(props, children) -> format "polygon" props children + | Polyline(props, children) -> format "polyline" props children + | RadialGradient(props, children) -> format "radient" props children + | Rect(props, children) -> format "rect" props children + | Stop(props, children) -> format "stop" props children + | Text(props, children) -> format "text" props children + | Tspan(props, children) -> format "tspan" props children + | String str -> str + | EncodeString str -> System.Web.HttpUtility.HtmlEncode str + | CustomElement(element, props, children) -> format element props children + + helper tag + override tag.ToString() = let rec format tag (props: HtmlProperties list) (children: HtmlElement list) level = let cnt = @@ -745,6 +915,7 @@ module internal Html = let tspan (props: HtmlProperties list) (children: HtmlElement list) = HtmlElement.Tspan(props, children) //let string str = HtmlElement.String str let (!!) str = HtmlElement.String str + let rawString str = HtmlElement.EncodeString str let encode str = HtmlElement.EncodeString str /// Web component from https://iconify.design/docs/ diff --git a/src/fsdocs-tool/Options.fs b/src/fsdocs-tool/Options.fs index 97928d04..a0a44b55 100644 --- a/src/fsdocs-tool/Options.fs +++ b/src/fsdocs-tool/Options.fs @@ -32,5 +32,5 @@ module Common = let waitForKey b = if b then - printf "\nPress any key to continue ..." + printfn "\nPress any key to continue ..." System.Console.ReadKey() |> ignore