diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84a733c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +_site +_cache +.DS_Store +deno.lock \ No newline at end of file diff --git a/_config.js b/_config.js new file mode 100644 index 0000000..1af1a10 --- /dev/null +++ b/_config.js @@ -0,0 +1,28 @@ +import lume from "lume/mod.ts"; +import base_path from "lume/plugins/base_path.ts"; +import metas from "lume/plugins/metas.ts"; +import postcss from "lume/plugins/postcss.ts"; +import autoDependency from "https://deno.land/x/oi_lume_utils@v0.4.0/processors/auto-dependency.ts"; + +const site = lume({ + src: './src', + location: new URL("https://open-innovations.org.github.io/markdown-to-vento") + }); + +site.process([".html"], (pages) => pages.forEach(autoDependency)); + +site.use(base_path()); + +site.use(metas({ + defaultPageData: { + title: 'title', + }, + })); + +site.use(postcss()); + +site.copy('.nojekyll'); + +site.copy('assets/js'); + +export default site; diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..0c488f4 --- /dev/null +++ b/deno.json @@ -0,0 +1,15 @@ +{ + "imports": { + "lume/": "https://deno.land/x/lume@v2.3.0/" + }, + "tasks": { + "lume": "echo \"import 'lume/cli.ts'\" | deno run -A -", + "build": "deno task lume", + "serve": "deno task lume -s" + }, + "compilerOptions": { + "types": [ + "lume/types.ts" + ] + } +} diff --git a/src/.nojekyll b/src/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/src/_data.yaml b/src/_data.yaml new file mode 100644 index 0000000..4e64cde --- /dev/null +++ b/src/_data.yaml @@ -0,0 +1,49 @@ +layout: template/page.vto + +# Override the site logo colour here. Any CSS colour will work. +logo_colour: black + +# Override the header class here. See https://open-innovations.org/brand for colour classes +header_class: c5-bg + +# Automatically adds meta and social tags +# See https://lume.land/plugins/metas/ +metas: + site: Markdown to Vento Converter + description: A simple tool to convert markdown files to vento with appropriate styling. + image: https://open-innovations.org/resources/images/logos/oi-square.png + twitter: openinnovates + lang: en + keywords: [open, data, leeds, innovation] + +footer: "
+

Notes:

+ +
+" \ No newline at end of file diff --git a/src/_includes/css/building-blocks/stack.css b/src/_includes/css/building-blocks/stack.css new file mode 100644 index 0000000..2e41995 --- /dev/null +++ b/src/_includes/css/building-blocks/stack.css @@ -0,0 +1,7 @@ +/* https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/ */ +.stack * + * { + margin-top: 1.5em; + } + .no-stack > * { + margin-top: 0; + } \ No newline at end of file diff --git a/src/_includes/css/file-drop.css b/src/_includes/css/file-drop.css new file mode 100644 index 0000000..26ed8fe --- /dev/null +++ b/src/_includes/css/file-drop.css @@ -0,0 +1,35 @@ +#drop-area { + border: 2px dashed #ccc; + background-color: #fff; + width: 600px; + text-align: center; + margin: auto; + margin-top: 2rem; +} + +#drop-area.hover { + border-color: #333; +} + +#drop-area p { + margin: 10px 0; +} + +#download-link { + display: none; /* Initially hidden */ + margin-top: 1rem; + padding: 0.5rem 1rem; + font-size: 18px; + background-color: whitesmoke; + text-align: center; + text-decoration: none; + border: 0.1rem solid #ccc; + cursor: pointer; +} + +#download-link:hover { + background-color: #e0e0e0; +} +#fileElem { + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/_includes/css/oi.css b/src/_includes/css/oi.css new file mode 100644 index 0000000..ab29653 --- /dev/null +++ b/src/_includes/css/oi.css @@ -0,0 +1,172 @@ +@font-face { + font-family: 'Poppins'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiDyp8kv8JHgFVrJJLmr19VFteOcEg.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; +} +@font-face { + font-family: 'Poppins'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiDyp8kv8JHgFVrJJLmr19VGdeOcEg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Poppins'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiDyp8kv8JHgFVrJJLmr19VF9eO.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; +} +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(https://open-innovations.org/resources/fonts/Poppins/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --oi-main-fontstack: "Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; + --oi-title-fontstack: Poppins; +} +html, body { + padding: 0px; + margin: 0px; + min-height: 100%!important; + height: 100%!important; + background-color: #ffffff; + font-family: var(--oi-main-fontstack); + /* https://css-tricks.com/accessible-font-sizing-explained/#aa-avoid-setting-a-base-font-size */ + font-size: 100%; + color: #333; + line-height: 1.44em; +} +h1,h2,h3,h4, .title, .subtitle { font-family: var(--oi-title-fontstack); line-height: 1em; } +h1, .title { font-size: 2em; margin-top: 1em; font-weight: 600; } +h2 { font-size: 1.7em; margin-top: 1.764706em; font-weight: 500; } +h3 { font-size: 1.4em; margin-top: 1.428571em; } +h1:first-child, h2:first-child, h3:first-child, .title { margin-top: 0; } +h1 + *, h2 + *, h3 + * { margin-top: 1em; } +strong { font-weight: 700; } +em { font-style: italic; } +a { color: #18AD87; text-decoration: underline; } +.bold { color: #18AD87; } +p, time, li, td, th, footer, div { font-weight: 300; } +p, iframe, h2, h3, figure, ul, ol, .block, .padded-bottom { margin-bottom: 1em; } +button, .button { border: 0px; font-size: 1em; background-color: #efefef; color: black; text-decoration: none!important; display: inline-block; padding: 0.5em 1em; cursor: pointer; vertical-align: top; line-height: 1.25em; } +a:hover, a:focus { text-decoration: underline; } +a:visited { color: inherit; } +header a { text-decoration: none; line-height: 0; } +footer { width: 100%; padding-bottom: 3em; } +footer h2 + p { margin-top: 1em; } +footer a { color: #1DD3A7; } +code,pre { font-family: monospace; color: #444; background-color: #efefef; } +code { padding: 0.125em 0.25em; } +pre { max-width: 100%; overflow-x: auto; padding: 16px; margin-bottom: 16px; } +pre code { padding: 0; } +ul { list-style: disc; } +ol { list-style: decimal; } +ul { padding: 0; margin-left: 2em; } +.msg { text-align:center; padding: 0.5em; } + +.holder { position: relative; width: 1080px; max-width: 100%; margin: auto; } +.holder.padded > *:last-child { margin-bottom: 0; } +button, .button { margin-top: 0.25em; border: 0px; font-size: 1em; background-color: #efefef; color: black; text-decoration: none!important; display: inline-block; padding: 0.5em 1em; cursor: pointer; } +a.button:hover, button:hover, button:focus, a.button:focus { background-color: #333333!important; color: #efefef!important; } +.b1-bg { background-color: #000000; color: white!important; } +.b2-bg { background-color: #333333; color: white!important; } +.bk-bg { background-color: #6D6D6D; color: white!important; } +.b3-bg, .github:hover { background-color: #999999; color: black!important; } +.b4-bg { background-color: #bbbbbb; color: black!important; } +.b5-bg, .off label { background-color: #dfdfdf; color: black!important; } +.b6-bg { background-color: #ffffff; color: black!important; } +.c1-bg, .elsewhere .facebook:hover, .c1-bg-hover:hover, .c1-bg:visited, .seasonal, .seasonal:visited, .seasonal:hover { background-color: #2254F4; color: white; } +.c2-bg, .c2-bg-hover:hover, .c2-bg:visited { background-color: #178CFF; color: black; } +.c3-bg, .elsewhere .twitter:hover, .c3-bg-hover:hover, .c3-bg:visited { background-color: #00B6FF; color: black; } +.c4-bg, .c4-bg-hover:hover, .c4-bg:visited, .seasonal-accent, .seasonal-accent:hover, .seasonal-accent:visited { background-color: #08DEF9; color: black; } +.c5-bg, .c5-bg-hover:hover, .c5-bg:visited { background-color: #1DD3A7; color: black; } +.c6-bg, .c6-bg-hover:hover, .c6-bg:visited { background-color: #0DBC37; color: black; } +.c7-bg, .c7-bg-hover:hover, .c7-bg:visited { background-color: #67E767; color: black; } +.c8-bg, .c8-bg-hover:hover, .c8-bg:visited { background-color: #722EA5; color: white; } +.c9-bg, .c9-bg-hover:hover, .c9-bg:visited, .instagram:hover { background-color: #E6007C; color: white; } +.c10-bg, .c10-bg-hover:hover, .c10-bg:visited, .flickr:hover { background-color: #EF3AAB; color: black; } +.c11-bg, .c11-bg-hover:hover, .c11-bg:visited { background-color: #D73058; color: white; } +.c12-bg, .c12-bg-hover:hover, .c12-bg:visited, .youtube:hover { background-color: #D60303; color: white; } +.c13-bg, .c13-bg-hover:hover, .c13-bg:visited { background-color: #FF6700; color: black; } +.c14-bg, .c14-bg-hover:hover, .c14-bg:visited { background-color: #F9BC26; color: black; } +.c12-bg, .on label { background-color: #D60303; color: white; } +.c12-bg:hover, .c12-bg:focus { background: #444; color: white; } +.warning { color: rgb(95, 82, 7); background-color: rgb(251, 245, 208); filter: drop-shadow(0px 0px 1px rgb(95, 82, 7)); } +.padded { padding: 1em; } +.padded-bottom { padding: 1em; margin-bottom: 1em; } +.doublepadded { padding: 2em; } +.spaced { margin-bottom: 1em; } +.centred { text-align: center; } +.tallpadded { padding-top: 3em; padding-bottom: 3em; } +section { padding: 2em 0; } + +.skip-to-content-link { position: absolute; top: -3rem; right: 0.5rem; z-index: 2000; transition: top 0.3s; padding: 0.5rem 1rem; } +.skip-to-content-link:focus { top: 0.25rem; } + +header h1 { margin-top: 0; } +header nav ul { + width: min-content; + text-align: center; + margin: 0 auto; + font-size: 1.2em; + position: relative; + font-family: Poppins; + display: flex; +} +header nav ul li { display: inline-block; } +header nav ul > li > a { padding: 0.5em 1em; display: inline-block; width: 100%; color: inherit; } +header nav ul > li > a:hover, header nav ul > li > a:focus { background: white; color: black; } + +.grid { display: grid; grid-template-columns: repeat(2,1fr); grid-gap: 1em; } +.grid .pane { background: #f9f9f9; } + +.grid-list { display: grid; grid-template-columns: repeat(3,1fr); grid-gap: 0.5em; list-style: none; margin: 0; margin-bottom: 2em; } +.grid-list > li > a { padding: 1em; background: #dfdfdf; text-decoration: none; display: block; text-align: center; height: 100%; } + +.legend { margin-bottom: 1em; text-align: center; } +.legend .legend-item { display: inline-block; line-height: 1em; margin-right: 1em; cursor: pointer; } +.legend .legend-item svg { float: left; } + +.elsewhere { float: right; margin-top: 0; } +.elsewhere li { display: inline-block; margin-left: 0.5em; } +.elsewhere li:first-child { margin-left: 0; } +.elsewhere li a { line-height: 0; display: block; } +footer .address { margin-top: 1em; } +footer a { color: inherit; } +footer hr.about { margin: 32px 0; border: 0; border-top:1px solid #444; } + +@media only screen and (max-width: 1080px) { + .grid { grid-template-columns: 100%; } + .grid-list { grid-template-columns: repeat(2,1fr); grid-gap: 0.25em; } +} + +@media only screen and (max-width: 800px) { + .elsewhere { float:none; margin-left: 0; margin-bottom: 1em; } +} \ No newline at end of file diff --git a/src/_includes/css/reset.css b/src/_includes/css/reset.css new file mode 100644 index 0000000..26cd6b8 --- /dev/null +++ b/src/_includes/css/reset.css @@ -0,0 +1,75 @@ +/* https://piccalil.li/blog/a-modern-css-reset/ */ +/* Box sizing rules */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* Remove default margin */ +body, +h1, +h2, +h3, +h4, +p, +figure, +blockquote, +dl, +dd { + margin: 0; +} + +/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ +ul[role='list'], +ol[role='list'] { + list-style: none; +} + +/* Set core root defaults */ +html:focus-within { + scroll-behavior: smooth; +} + +/* Set core body defaults */ +body { + min-height: 100vh; + text-rendering: optimizeSpeed; + line-height: 1.5; +} + +/* A elements that don't have a class get default styles */ +a:not([class]) { + text-decoration-skip-ink: auto; +} + +/* Make images easier to work with */ +img, +picture { + max-width: 100%; + display: block; +} + +/* Inherit fonts for inputs and buttons */ +input, +button, +textarea, +select { + font: inherit; +} + +/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ +@media (prefers-reduced-motion: reduce) { + html:focus-within { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} \ No newline at end of file diff --git a/src/_includes/partials/footer.vto b/src/_includes/partials/footer.vto new file mode 100644 index 0000000..891276c --- /dev/null +++ b/src/_includes/partials/footer.vto @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/src/_includes/partials/header.vto b/src/_includes/partials/header.vto new file mode 100644 index 0000000..7e8369a --- /dev/null +++ b/src/_includes/partials/header.vto @@ -0,0 +1,10 @@ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/_includes/template/base.vto b/src/_includes/template/base.vto new file mode 100644 index 0000000..70d3ce0 --- /dev/null +++ b/src/_includes/template/base.vto @@ -0,0 +1,26 @@ + + + + + + + {{# I use the metas key here so that this is consistent with social media sharing #}} + {{ title }} | {{ metas.site }} + + + + + + + {{ include 'partials/header.vto' }} + +
+
+ {{ content |> safe }} +
+
+ + {{ include 'partials/footer.vto' }} + + + \ No newline at end of file diff --git a/src/_includes/template/page.vto b/src/_includes/template/page.vto new file mode 100644 index 0000000..64d4ef1 --- /dev/null +++ b/src/_includes/template/page.vto @@ -0,0 +1,5 @@ +--- +layout: template/base.vto +--- +

{{ title }}

+{{ content }} \ No newline at end of file diff --git a/src/assets/css/style.css b/src/assets/css/style.css new file mode 100644 index 0000000..2da058c --- /dev/null +++ b/src/assets/css/style.css @@ -0,0 +1,11 @@ +@charset "utf-8"; +/* These are imported by the PostCSS processor. Source is in the `_includes/css` folder */ +@import 'css/reset.css'; +@import 'css/building-blocks/stack.css'; + +/* + All the standard OI stuff is included here. + Could be modularised, given PostCSS could enable much smaller css packages to be created. +*/ +@import 'css/oi.css'; +@import 'css/file-drop.css'; \ No newline at end of file diff --git a/src/assets/js/app.js b/src/assets/js/app.js new file mode 100644 index 0000000..5c82882 --- /dev/null +++ b/src/assets/js/app.js @@ -0,0 +1,105 @@ +document.addEventListener('DOMContentLoaded', () => { + const dropArea = document.getElementById('drop-area'); + const fileElem = document.getElementById('fileElem'); + const downloadLink = document.getElementById('download-link'); + + dropArea.addEventListener('dragover', (e) => { + e.preventDefault(); + dropArea.classList.add('hover'); + }); + + dropArea.addEventListener('dragleave', () => { + dropArea.classList.remove('hover'); + }); + + dropArea.addEventListener('drop', (e) => { + e.preventDefault(); + dropArea.classList.remove('hover'); + const files = e.dataTransfer.files; + handleFiles(files); + }); + + fileElem.addEventListener('change', () => { + const files = fileElem.files; + handleFiles(files); + }); + + function handleFiles(files) { + if (files.length > 0) { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = (e) => { + const content = e.target.result; + const convertedContent = convertMarkdownToVTO(content); + createDownloadableFile(convertedContent); + }; + + reader.readAsText(file); + } + } + + function convertMarkdownToVTO(markdown) { + let lines = markdown.split('\n'); + let inFrontMatter = false; + let inCodeBlock = false; + let inHtmlTag = false; + let result = []; + + const openHtmlTagPattern = /<([a-zA-Z][a-zA-Z0-9]*)[^>]*>/; + const closeHtmlTagPattern = /<\/([a-zA-Z][a-zA-Z0-9]*)>/; + + for (let line of lines) { + if (line.trim() === '---') { + inFrontMatter = !inFrontMatter; + result.push(line); + continue; + } + + if (inFrontMatter) { + result.push(line); + continue; + } + + if (line.trim().startsWith('```')) { + if (!inCodeBlock) { + const language = line.trim().slice(3); + result.push(`
`);
+                } else {
+                    result.push('
'); + } + inCodeBlock = !inCodeBlock; + continue; + } + + if (openHtmlTagPattern.test(line)) { + inHtmlTag = true; + result.push(line); + continue; + } + + if (closeHtmlTagPattern.test(line)) { + inHtmlTag = false; + result.push(line); + continue; + } + + if (inCodeBlock || inHtmlTag) { + result.push(line); + } else { + if (line.trim() !== '') { + result.push(`

${line}

`); + } + } + } + + return result.join('\n'); + } + + function createDownloadableFile(content) { + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.style.display = 'block'; // Make the download link visible + } +}); diff --git a/src/index.vto b/src/index.vto new file mode 100644 index 0000000..1af999e --- /dev/null +++ b/src/index.vto @@ -0,0 +1,26 @@ +--- +title: Markdown to Vento Converter +--- +

+ On our housing site we recently started + documenting some of our code using + jupyter notebooks. It's easy to convert these to markdown format using jupyter nbconvert, and Lume + supports markdown as a template format. This worked fine until we wanted to put + OI Lume charts onto those pages as well. At this point, we needed to convert the files to Vento to allow us to load + the charts and maintain the styling for the code blocks. This was a tedious manual process, so we made a tool to do it for us. + This programme runs in the browser - nothing ever leaves your computer. +

+

+ First, convert your notebook using the command line: jupyter nbconvert your_notebok.ipynb --to markdown. + The output will be saved in your current working directory. Read the docs + for more detailed info. Then use this tool to convert the markdown to vento. +

+
+

Drop a markdown file here

+

or

+ +
+ +

Download Converted File

+ +