From 3bf7773545d55ae569fec09beeb950a5ddf52435 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 20 Nov 2022 15:30:30 -0500 Subject: [PATCH 1/3] Introduce LinkedFile resource as start to showing code samples. --- .../examples/interest_calculator/index.md | 3 ++ src/psc/resources.py | 46 +++++++++++++++---- tests/test_resources.py | 25 ++++++---- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/psc/gallery/examples/interest_calculator/index.md b/src/psc/gallery/examples/interest_calculator/index.md index bffb179..dc35249 100644 --- a/src/psc/gallery/examples/interest_calculator/index.md +++ b/src/psc/gallery/examples/interest_calculator/index.md @@ -2,5 +2,8 @@ title: Compound Interest Calculator subtitle: Enter some numbers, get some numbers. author: meg-1 +linked_files: + - calculator.py + - styles.css --- The *body* description. diff --git a/src/psc/resources.py b/src/psc/resources.py index 5717f8a..4fecd53 100644 --- a/src/psc/resources.py +++ b/src/psc/resources.py @@ -17,13 +17,12 @@ from psc.here import HERE from psc.here import PYODIDE - EXCLUSIONS = ("pyscript.css", "pyscript.js", "favicon.png") def tag_filter( - tag: Tag, - exclusions: tuple[str, ...] = EXCLUSIONS, + tag: Tag, + exclusions: tuple[str, ...] = EXCLUSIONS, ) -> bool: """Filter nodes from example that should not get included.""" attr = "href" if tag.name == "link" else "src" @@ -81,6 +80,27 @@ class Resource: extra_head: str = "" +linked_file_mapping = dict( + py="python", + css="css", + html="html" +) + + +@dataclass +class LinkedFile: + """A source file on disk that gets attached to an example.""" + + path: Path + language: str = field(init=False) + body: str = field(init=False) + + def __post_init__(self) -> None: + """Read the file contents into the body.""" + self.language = linked_file_mapping[self.path.suffix[1:]] + self.body = self.path.read_text() + + @dataclass class Example(Resource): """Create an example from an HTML location on disk. @@ -91,9 +111,10 @@ class Example(Resource): Meaning, HERE / "examples" / name / "index.html". """ - subtitle: str = "" - description: str = "" - author: str | None = None + subtitle: str = field(init=False) + description: str = field(init=False) + author: str = field(init=False) + linked_files: list[LinkedFile] = field(default_factory=list) def __post_init__(self) -> None: """Extract most of the data from the HTML file.""" @@ -107,13 +128,22 @@ def __post_init__(self) -> None: self.description = str(md.render(md_fm.content)) # Main, extra head example's HTML file. - index_html_file = HERE / "gallery/examples" / self.name / "index.html" + this_example_path = HERE / "gallery/examples" / self.name + index_html_file = this_example_path / "index.html" if not index_html_file.exists(): # pragma: nocover raise ValueError(f"No example at {self.name}") - soup = BeautifulSoup(index_html_file.read_text(), "html5lib") + index_html_text = index_html_file.read_text() + soup = BeautifulSoup(index_html_text, "html5lib") self.extra_head = get_head_nodes(soup) self.body = get_body_content(soup) + # Process any linked files + linked_paths = [*["index.html"], *md_fm.get("linked_files", [])] + for linked_name in linked_paths: + linked_path = this_example_path / linked_name + linked_file = LinkedFile(path=linked_path) + self.linked_files.append(linked_file) + @dataclass class Author(Resource): diff --git a/tests/test_resources.py b/tests/test_resources.py index bfc7460..5a3dc1f 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -5,7 +5,7 @@ from bs4 import BeautifulSoup from psc.here import HERE -from psc.resources import Example +from psc.resources import Example, LinkedFile from psc.resources import Page from psc.resources import Resources from psc.resources import get_body_content @@ -15,7 +15,6 @@ from psc.resources import is_local from psc.resources import tag_filter - IS_LOCAL = is_local() @@ -122,14 +121,24 @@ def test_example_bad_path() -> None: def test_example() -> None: """Construct an ``Example`` and ensure it has all the template bits.""" - this_example = Example(name="hello_world") - assert this_example.title == "Hello World" + this_example = Example(name="interest_calculator") + assert this_example.title == "Compound Interest Calculator" assert ( - this_example.subtitle - == "The classic hello world, but in Python -- in a browser!" + this_example.subtitle + == "Enter some numbers, get some numbers." ) - assert "hello_world.css" in this_example.extra_head - assert "

Hello ...

" in this_example.body + assert "styles.css" in this_example.extra_head + assert "Welcome to the" in this_example.body + + linked_files = this_example.linked_files + assert "html" == linked_files[0].language + + +def test_linked_file() -> None: + """Correctly create a LinkedFile.""" + linked_path = HERE / "gallery/examples/altair/index.html" + linked_file = LinkedFile(path=linked_path) + assert "html" == linked_file.language def test_markdown_page() -> None: From 1ae83e3f62021262d866a24a74ea817676f65533 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 20 Nov 2022 17:31:09 -0500 Subject: [PATCH 2/3] Make a new view to show the LinkedFile code. Add Prism. Provide buttons to switch between code and demo. --- src/psc/app.py | 20 +++ .../examples/hello_world/hello_world.css | 1 + src/psc/gallery/examples/hello_world/index.md | 3 + .../examples/interest_calculator/index.md | 2 +- src/psc/resources.py | 8 +- src/psc/static/prism.css | 160 ++++++++++++++++++ src/psc/static/prism.js | 12 ++ src/psc/templates/example.jinja2 | 6 +- src/psc/templates/example_code.jinja2 | 19 +++ tests/test_generic_example.py | 22 ++- tests/test_resources.py | 9 +- 11 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 src/psc/static/prism.css create mode 100644 src/psc/static/prism.js create mode 100644 src/psc/templates/example_code.jinja2 diff --git a/src/psc/app.py b/src/psc/app.py index 7b5da85..8799846 100644 --- a/src/psc/app.py +++ b/src/psc/app.py @@ -132,6 +132,25 @@ async def example(request: Request) -> _TemplateResponse: ) +async def example_code(request: Request) -> _TemplateResponse: + """Handle the linked files for the code example.""" + example_name = request.path_params["example_name"] + resources: Resources = request.app.state.resources + this_example = resources.examples[example_name] + root_path = "../../.." + + return templates.TemplateResponse( + "example_code.jinja2", + dict( + title=f"{this_example.title} Code", + extra_head=this_example.extra_head, + request=request, + root_path=root_path, + linked_files=this_example.linked_files, + ), + ) + + async def content_page(request: Request) -> _TemplateResponse: """Handle a content page.""" page_name = request.path_params["page_name"] @@ -159,6 +178,7 @@ async def content_page(request: Request) -> _TemplateResponse: Route("/authors", authors), Route("/authors/{author_name}.html", author), Route("/gallery/examples/{example_name}/index.html", example), + Route("/gallery/examples/{example_name}/code.html", example_code), Route("/gallery/examples/{example_name}/", example), Route("/pages/{page_name}.html", content_page), Mount("/gallery", StaticFiles(directory=HERE / "gallery")), diff --git a/src/psc/gallery/examples/hello_world/hello_world.css b/src/psc/gallery/examples/hello_world/hello_world.css index 5da122d..e26f294 100644 --- a/src/psc/gallery/examples/hello_world/hello_world.css +++ b/src/psc/gallery/examples/hello_world/hello_world.css @@ -1,2 +1,3 @@ h1 { + font-weight: normal; } diff --git a/src/psc/gallery/examples/hello_world/index.md b/src/psc/gallery/examples/hello_world/index.md index 3d91b0a..42088cf 100644 --- a/src/psc/gallery/examples/hello_world/index.md +++ b/src/psc/gallery/examples/hello_world/index.md @@ -1,5 +1,8 @@ --- title: Hello World subtitle: The classic hello world, but in Python -- in a browser! +linked_files: + - hello_world.css + - hello_world.js --- The *body* description. diff --git a/src/psc/gallery/examples/interest_calculator/index.md b/src/psc/gallery/examples/interest_calculator/index.md index dc35249..1c818ea 100644 --- a/src/psc/gallery/examples/interest_calculator/index.md +++ b/src/psc/gallery/examples/interest_calculator/index.md @@ -2,7 +2,7 @@ title: Compound Interest Calculator subtitle: Enter some numbers, get some numbers. author: meg-1 -linked_files: +linked_files: - calculator.py - styles.css --- diff --git a/src/psc/resources.py b/src/psc/resources.py index 4fecd53..4c24b65 100644 --- a/src/psc/resources.py +++ b/src/psc/resources.py @@ -17,12 +17,13 @@ from psc.here import HERE from psc.here import PYODIDE + EXCLUSIONS = ("pyscript.css", "pyscript.js", "favicon.png") def tag_filter( - tag: Tag, - exclusions: tuple[str, ...] = EXCLUSIONS, + tag: Tag, + exclusions: tuple[str, ...] = EXCLUSIONS, ) -> bool: """Filter nodes from example that should not get included.""" attr = "href" if tag.name == "link" else "src" @@ -83,7 +84,8 @@ class Resource: linked_file_mapping = dict( py="python", css="css", - html="html" + html="html", + js="javascript", ) diff --git a/src/psc/static/prism.css b/src/psc/static/prism.css new file mode 100644 index 0000000..f56af87 --- /dev/null +++ b/src/psc/static/prism.css @@ -0,0 +1,160 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+python&plugins=custom-class+toolbar+copy-to-clipboard+download-button */ +code[class*=language-], pre[class*=language-] { + color: #000; + background: 0 0; + text-shadow: 0 1px #fff; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none +} + +code[class*=language-] ::-moz-selection, code[class*=language-]::-moz-selection, pre[class*=language-] ::-moz-selection, pre[class*=language-]::-moz-selection { + text-shadow: none; + background: #b3d4fc +} + +code[class*=language-] ::selection, code[class*=language-]::selection, pre[class*=language-] ::selection, pre[class*=language-]::selection { + text-shadow: none; + background: #b3d4fc +} + +@media print { + code[class*=language-], pre[class*=language-] { + text-shadow: none + } +} + +pre[class*=language-] { + padding: 1em; + margin: .5em 0; + overflow: auto +} + +:not(pre) > code[class*=language-], pre[class*=language-] { + background: #f5f2f0 +} + +:not(pre) > code[class*=language-] { + padding: .1em; + border-radius: .3em; + white-space: normal +} + +.prism--token.prism--cdata, .prism--token.prism--comment, .prism--token.prism--doctype, .prism--token.prism--prolog { + color: #708090 +} + +.prism--token.prism--punctuation { + color: #999 +} + +.prism--token.prism--namespace { + opacity: .7 +} + +.prism--token.prism--boolean, .prism--token.prism--constant, .prism--token.prism--deleted, .prism--token.prism--number, .prism--token.prism--property, .prism--token.prism--symbol, .prism--token.prism--tag { + color: #905 +} + +.prism--token.prism--attr-name, .prism--token.prism--builtin, .prism--token.prism--char, .prism--token.prism--inserted, .prism--token.prism--selector, .prism--token.prism--string { + color: #690 +} + +.language-css .prism--token.prism--string, .style .prism--token.prism--string, .prism--token.prism--entity, .prism--token.prism--operator, .prism--token.prism--url { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5) +} + +.prism--token.prism--atrule, .prism--token.prism--attr-value, .prism--token.prism--keyword { + color: #07a +} + +.prism--token.prism--class-name, .prism--token.prism--function { + color: #dd4a68 +} + +.prism--token.prism--important, .prism--token.prism--regex, .prism--token.prism--variable { + color: #e90 +} + +.prism--token.prism--bold, .prism--token.prism--important { + font-weight: 700 +} + +.prism--token.prism--italic { + font-style: italic +} + +.prism--token.prism--entity { + cursor: help +} + +div.code-toolbar { + position: relative +} + +div.code-toolbar > .toolbar { + position: absolute; + z-index: 10; + top: .3em; + right: .2em; + transition: opacity .3s ease-in-out; + opacity: 0 +} + +div.code-toolbar:hover > .toolbar { + opacity: 1 +} + +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: 0 0; + border: 0; + color: inherit; + font: inherit; + line-height: normal; + overflow: visible; + padding: 0; + -webkit-user-select: none; + -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, .2); + box-shadow: 0 2px 0 0 rgba(0, 0, 0, .2); + border-radius: .5em +} + +div.code-toolbar > .toolbar > .toolbar-item > a:focus, div.code-toolbar > .toolbar > .toolbar-item > a:hover, div.code-toolbar > .toolbar > .toolbar-item > button:focus, div.code-toolbar > .toolbar > .toolbar-item > button:hover, div.code-toolbar > .toolbar > .toolbar-item > span:focus, div.code-toolbar > .toolbar > .toolbar-item > span:hover { + color: inherit; + text-decoration: none +} diff --git a/src/psc/static/prism.js b/src/psc/static/prism.js new file mode 100644 index 0000000..96848f2 --- /dev/null +++ b/src/psc/static/prism.js @@ -0,0 +1,12 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+python&plugins=custom-class+toolbar+copy-to-clipboard+download-button */ +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; +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(){if("undefined"!=typeof Prism){var n,s,a="";Prism.plugins.customClass={add:function(s){n=s},map:function(n){s="function"==typeof n?n:function(s){return n[s]||s}},prefix:function(n){a=n||""},apply:t},Prism.hooks.add("wrap",(function(e){if(n){var u=n({content:e.content,type:e.type,language:e.language});Array.isArray(u)?e.classes.push.apply(e.classes,u):u&&e.classes.push(u)}(s||a)&&(e.classes=e.classes.map((function(n){return t(n,e.language)})))}))}function t(n,t){return a+(s?s(n,t):n)}}(); +!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e=[],t={},n=function(){};Prism.plugins.toolbar={};var a=Prism.plugins.toolbar.registerButton=function(n,a){var r;r="function"==typeof a?a:function(e){var t;return"function"==typeof a.onClick?((t=document.createElement("button")).type="button",t.addEventListener("click",(function(){a.onClick.call(this,e)}))):"string"==typeof a.url?(t=document.createElement("a")).href=a.url:t=document.createElement("span"),a.className&&t.classList.add(a.className),t.textContent=a.text,t},n in t?console.warn('There is a button with the key "'+n+'" registered already.'):e.push(t[n]=r)},r=Prism.plugins.toolbar.hook=function(a){var r=a.element.parentNode;if(r&&/pre/i.test(r.nodeName)&&!r.parentNode.classList.contains("code-toolbar")){var o=document.createElement("div");o.classList.add("code-toolbar"),r.parentNode.insertBefore(o,r),o.appendChild(r);var i=document.createElement("div");i.classList.add("toolbar");var l=e,d=function(e){for(;e;){var t=e.getAttribute("data-toolbar-order");if(null!=t)return(t=t.trim()).length?t.split(/\s*,\s*/g):[];e=e.parentElement}}(a.element);d&&(l=d.map((function(e){return t[e]||n}))),l.forEach((function(e){var t=e(a);if(t){var n=document.createElement("div");n.classList.add("toolbar-item"),n.appendChild(t),i.appendChild(n)}})),o.appendChild(i)}};a("label",(function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-label")){var n,a,r=t.getAttribute("data-label");try{a=document.querySelector("template#"+r)}catch(e){}return a?n=a.content:(t.hasAttribute("data-url")?(n=document.createElement("a")).href=t.getAttribute("data-url"):n=document.createElement("span"),n.textContent=r),n}})),Prism.hooks.add("complete",r)}}(); +!function(){function t(t){var e=document.createElement("textarea");e.value=t.getText(),e.style.top="0",e.style.left="0",e.style.position="fixed",document.body.appendChild(e),e.focus(),e.select();try{var o=document.execCommand("copy");setTimeout((function(){o?t.success():t.error()}),1)}catch(e){setTimeout((function(){t.error(e)}),1)}document.body.removeChild(e)}"undefined"!=typeof Prism&&"undefined"!=typeof document&&(Prism.plugins.toolbar?Prism.plugins.toolbar.registerButton("copy-to-clipboard",(function(e){var o=e.element,n=function(t){var e={copy:"Copy","copy-error":"Press Ctrl+C to copy","copy-success":"Copied!","copy-timeout":5e3};for(var o in e){for(var n="data-prismjs-"+o,c=t;c&&!c.hasAttribute(n);)c=c.parentElement;c&&(e[o]=c.getAttribute(n))}return e}(o),c=document.createElement("button");c.className="copy-to-clipboard-button",c.setAttribute("type","button");var r=document.createElement("span");return c.appendChild(r),u("copy"),function(e,o){e.addEventListener("click",(function(){!function(e){navigator.clipboard?navigator.clipboard.writeText(e.getText()).then(e.success,(function(){t(e)})):t(e)}(o)}))}(c,{getText:function(){return o.textContent},success:function(){u("copy-success"),i()},error:function(){u("copy-error"),setTimeout((function(){!function(t){window.getSelection().selectAllChildren(t)}(o)}),1),i()}}),c;function i(){setTimeout((function(){u("copy")}),n["copy-timeout"])}function u(t){r.textContent=n[t],c.setAttribute("data-copy-state",t)}})):console.warn("Copy to Clipboard plugin loaded before Toolbar plugin."))}(); +"undefined"!=typeof Prism&&"undefined"!=typeof document&&document.querySelector&&Prism.plugins.toolbar.registerButton("download-file",(function(t){var e=t.element.parentNode;if(e&&/pre/i.test(e.nodeName)&&e.hasAttribute("data-src")&&e.hasAttribute("data-download-link")){var n=e.getAttribute("data-src"),a=document.createElement("a");return a.textContent=e.getAttribute("data-download-link-label")||"Download",a.setAttribute("download",""),a.href=n,a}})); diff --git a/src/psc/templates/example.jinja2 b/src/psc/templates/example.jinja2 index 1e3fa11..ed83904 100644 --- a/src/psc/templates/example.jinja2 +++ b/src/psc/templates/example.jinja2 @@ -2,12 +2,14 @@ {% block extra_head %} - {{ extra_head | safe }}{% endblock %} + {{ extra_head | safe }} +{% endblock %} {% block main %}
+ Show Code

{{ title }}

{% if author %} -

By {{ author.title }}

+

By {{ author.title }}

{% endif %}

{{ subtitle }}

{{ body | safe }}
diff --git a/src/psc/templates/example_code.jinja2 b/src/psc/templates/example_code.jinja2 new file mode 100644 index 0000000..e35b585 --- /dev/null +++ b/src/psc/templates/example_code.jinja2 @@ -0,0 +1,19 @@ +{% extends "layout.jinja2" %} +{% block extra_head %} + + + +{% endblock %} +{% block main %} +
+ Back to Demo +

{{ title }}

+
+ {% for linked_file in linked_files %} +

{{ linked_file.path.name }}

+
{{ linked_file.body }}
+ {% endfor %} +
+
+{% endblock %} diff --git a/tests/test_generic_example.py b/tests/test_generic_example.py index f9d1a84..4d38991 100644 --- a/tests/test_generic_example.py +++ b/tests/test_generic_example.py @@ -10,8 +10,7 @@ def test_hello_world(client_page: PageT) -> None: # Title and subtitle title = soup.select_one("title") - assert title - assert title.text == "Hello World | PyScript Collective" + assert title and title.text == "Hello World | PyScript Collective" subtitle = soup.select_one('meta[name="subtitle"]') assert subtitle assert ( @@ -33,8 +32,27 @@ def test_hello_world(client_page: PageT) -> None: py_scripts = soup.select("py-script") assert len(py_scripts) == 1 + # Back to code button + button = soup.select_one("a.is-pulled-right") + assert button and "Show Code" == button.text + def test_hello_world_js(test_client: TestClient) -> None: """Test the static assets for Hello World.""" response = test_client.get("/gallery/examples/hello_world/hello_world.js") assert response.status_code == 200 + + +def test_hello_world_code(client_page: PageT) -> None: + """Test code samples for hello world example.""" + soup = client_page("/gallery/examples/hello_world/code.html") + title = soup.select_one("title") + assert title + assert "Hello World Code | PyScript Collective" == title.text.strip() + linked_file_heading = soup.select_one("h2") + assert linked_file_heading + assert "index.html" == linked_file_heading.text + + # Back to code button + button = soup.select_one("a.is-pulled-right") + assert button and "Back to Demo" == button.text diff --git a/tests/test_resources.py b/tests/test_resources.py index 5a3dc1f..f98948f 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -5,7 +5,8 @@ from bs4 import BeautifulSoup from psc.here import HERE -from psc.resources import Example, LinkedFile +from psc.resources import Example +from psc.resources import LinkedFile from psc.resources import Page from psc.resources import Resources from psc.resources import get_body_content @@ -15,6 +16,7 @@ from psc.resources import is_local from psc.resources import tag_filter + IS_LOCAL = is_local() @@ -123,10 +125,7 @@ def test_example() -> None: """Construct an ``Example`` and ensure it has all the template bits.""" this_example = Example(name="interest_calculator") assert this_example.title == "Compound Interest Calculator" - assert ( - this_example.subtitle - == "Enter some numbers, get some numbers." - ) + assert this_example.subtitle == "Enter some numbers, get some numbers." assert "styles.css" in this_example.extra_head assert "Welcome to the" in this_example.body From b1c288946d8f7606cdd8edd0bbf57e500a861d3e Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 20 Nov 2022 17:41:12 -0500 Subject: [PATCH 3/3] Update other examples to link to files. --- src/psc/gallery/examples/antigravity/index.md | 2 ++ src/psc/gallery/examples/hello_world_py/index.md | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/psc/gallery/examples/antigravity/index.md b/src/psc/gallery/examples/antigravity/index.md index 2901d8c..a37e0be 100644 --- a/src/psc/gallery/examples/antigravity/index.md +++ b/src/psc/gallery/examples/antigravity/index.md @@ -1,5 +1,7 @@ --- title: xkcd Antigravity subtitle: We can fly! +linked_files: + - antigravity.py --- Based on the [xkcd antigravity](https://xkcd.com/353/) diff --git a/src/psc/gallery/examples/hello_world_py/index.md b/src/psc/gallery/examples/hello_world_py/index.md index 71d5a12..752be6b 100644 --- a/src/psc/gallery/examples/hello_world_py/index.md +++ b/src/psc/gallery/examples/hello_world_py/index.md @@ -1,5 +1,9 @@ --- title: Hello World Python subtitle: The hello world example, but in a .py file. +linked_files: + - hello_world.py + - hello_world.css + - hello_world.js --- The *body* description.