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:
+
+
+ Front Matter Handling:
+
+ Front matter, marked by ---
, is included in the output without modification.
+
+
+
+ Code Block Handling:
+
+ Code blocks are detected by ```
delimiters.
+ Code blocks are wrapped in <pre><code>
tags, preserving their content.
+
+
+
+ HTML Tag Detection:
+
+ HTML content is left unchanged and not wrapped in additional tags.
+
+
+
+ Text Wrapping:
+
+ Non-code, non-HTML, non-front matter text is wrapped in <p>
tags.
+
+
+
+
+"
\ 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
+
+