Skip to content

Instantly share code, notes, and snippets.

@pfrazee
Created January 31, 2020 14:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pfrazee/1bf21e0881945893695c6f28748be3dc to your computer and use it in GitHub Desktop.
Save pfrazee/1bf21e0881945893695c6f28748be3dc to your computer and use it in GitHub Desktop.
Simple Wiki Theme
body {
--light-gray: #f7f7fc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
display: grid;
grid-gap: 10px;
grid-template-columns: 300px 1fr;
grid-template-rows: 100px 1fr;
min-height: 100vh;
min-width: 100vh;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
padding: 0.5rem 1rem;
background: #fff;
border: 1px solid #88f;
border-radius: 4px;
color: blue;
outline: 0;
font-size: 12px;
}
button:active {
background: var(--light-gray);
}
button.primary {
color: #fff;
background: blue;
border-color: blue;
}
header {
grid-column-start: 1;
grid-column-end: 3;
background: var(--light-gray);
}
nav {
border-right: 1px solid #ccd;
}
main {
padding-left: 20px;
}
wiki-header {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
height: 100px;
padding: 0 20px;
}
wiki-header h1,
wiki-header p {
margin: 0.25rem 0;
line-height: 1;
}
wiki-header h1 a {
color: inherit;
}
wiki-header .admin {
position: absolute;
top: 30px;
right: 20px;
}
wiki-header .admin button {
margin-left: 5px;
}
wiki-nav {
display: block;
overflow: hidden;
}
wiki-nav a {
display: block;
color: #445;
padding: 0.5rem 1rem;
margin: 0;
}
wiki-nav a:last-child {
border-bottom: 0;
}
wiki-nav a:hover {
text-decoration: none;
background: var(--light-gray);
}
wiki-nav a.active {
background: var(--light-gray);
}
wiki-nav .empty {
padding: 0.6rem 1rem;
color: #667;
}
.content > :first-child {
margin-top: 1rem;
}
.content hr {
border: 0;
border-top: 1px solid #ccd;
}
.content h1,
.content h2,
.content h3,
.content h4,
.content h5 { margin: 1.5rem 0; }
.content h1 { font-size: 2em; }
.content h2 { font-size: 1.7em; }
.content h3 { font-size: 1.4em; }
.content h4 { font-size: 1.3em; }
.content h5 { font-size: 1.1em; }
.content pre {
background: var(--light-gray);
padding: 1em;
overflow: auto;
}
.content p,
.content ul,
.content ol {
line-height: 1.5;
}
.content table {
margin: 1em 0;
}
.content blockquote {
border-left: 10px solid var(--light-gray);
margin: 1em 0;
padding: 1px 1.5em;
color: #667;
}
wiki-page .empty {
padding: 20vh 5vw 40vh 0;
text-align: center;
font-size: 23px;
color: #667;
}
wiki-page .empty button {
font-size: 18px;
}
wiki-page textarea.editor {
width: calc(100% - 20px);
height: calc(100vh - 130px);
margin-top: 10px;
padding: 20px;
box-sizing: border-box;
border: 1px solid #ccd;
font-size: 17px;
letter-spacing: 0.75px;
line-height: 1.4;
outline: 0;
}
<!doctype html>
<meta charset="utf8">
<html>
<head>
<script type="module" src="/theme/theme.js"></script>
<link rel="stylesheet" href="/theme/theme.css">
</head>
<body>
<header>
<wiki-header></wiki-header>
</header>
<nav>
<wiki-nav></wiki-nav>
</nav>
<main>
<wiki-page></wiki-page>
</main>
</body>
</html>
import MarkdownIt from './markdown-it.js'
var self = new Hyperdrive(location)
var pathname = location.pathname.endsWith('/') ? location.pathname + 'index.md' : location.pathname
var isEditing = location.search === '?edit'
function h (tag, attrs, ...children) {
var el = document.createElement(tag)
for (let k in attrs) {
if (k === 'cls') el.className = attrs[k]
else el.setAttribute(k, attrs[k])
}
for (let child of children) el.append(child)
return el
}
async function ensureParentDir (p) {
let parts = p.split('/').slice(0, -1)
let acc = []
for (let part of parts) {
acc.push(part)
await self.mkdir(acc.join('/')).catch(e => undefined)
}
}
customElements.define('wiki-header', class extends HTMLElement {
constructor () {
super()
this.load()
}
async load () {
this.info = await self.getInfo()
this.render()
}
render () {
this.append(h('h1', {}, h('a', {href: '/'}, this.info.title)))
if (this.info.description) {
this.append(h('p', {}, this.info.description))
}
if (this.info.writable) {
let buttons = []
if (!isEditing) {
let newPage = h('button', {}, 'New Page')
newPage.addEventListener('click', async (e) => {
var newPathname = prompt('Enter the path of the new page')
if (!newPathname) return
if (!newPathname.endsWith('.md')) newPathname += '.md'
await ensureParentDir(newPathname)
if ((await self.stat(newPathname).catch(e => undefined)) === undefined) {
await self.writeFile(newPathname, `# ${newPathname}`)
}
location = newPathname + '?edit'
})
buttons.push(newPage)
if (/\.(png|jpe?g|gif|mp4|mp3|ogg|webm|mov)$/.test(pathname) === false) {
let editPage = h('button', {}, 'Edit Page')
editPage.addEventListener('click', async (e) => {
location.search = '?edit'
})
buttons.push(editPage)
}
} else {
let savePage = h('button', {cls: 'primary'}, 'Save Page')
savePage.addEventListener('click', async (e) => {
let value = document.body.querySelector('textarea.editor').value
await self.writeFile(pathname, value)
location.search = ''
})
buttons.push(savePage)
}
let deletePage = h('button', {}, 'Delete Page')
deletePage.addEventListener('click', async (e) => {
if (!confirm('Delete this page?')) return
await self.unlink(pathname)
if (isEditing) location.search = ''
else location.reload()
})
buttons.push(deletePage)
let editProps = h('button', {}, 'Edit Drive Properties')
editProps.addEventListener('click', async (e) => {
await navigator.drivePropertiesDialog(self.url)
if (!isEditing) location.reload()
})
buttons.push(editProps)
this.append(h('div', {cls: 'admin'}, ...buttons))
}
}
})
customElements.define('wiki-nav', class extends HTMLElement {
constructor () {
super()
this.load()
}
async load () {
this.files = await self.readdir('/', {recursive: true})
this.files = this.files.filter(file => file.endsWith('.md'))
this.files.sort()
this.render()
}
render () {
for (let file of this.files) {
let href = `/${file}`
let cls = pathname === href ? 'active' : ''
this.append(h('a', {href, cls}, file.slice(0, -3)))
}
if (this.files.length === 0) {
this.append(h('div', {cls: 'empty'}, 'This Wiki has no pages'))
}
}
})
customElements.define('wiki-page', class extends HTMLElement {
constructor () {
super()
this.render()
}
async render () {
// check existence
let stat = await self.stat(pathname).catch(e => undefined)
if (!stat) {
// 404
let canEdit = (await self.getInfo()).writable
if (canEdit) {
let btn = h('button', {}, 'Create Page')
btn.addEventListener('click', async (e) => {
await ensureParentDir(pathname)
await self.writeFile(pathname, `# ${pathname}`)
location.search = '?edit'
})
this.append(h('div', {cls: 'empty'}, h('h2', {}, 'This Page Does Not Exist'), btn))
} else {
this.append(h('div', {cls: 'empty'}, h('h2', {}, 'This Page Does Not Exist')))
}
return
}
// embed content
if (/\.(png|jpe?g|gif)$/i.test(pathname)) {
this.append(h('img', {src: pathname}))
} else if (/\.(mp4|webm|mov)/i.test(pathname)) {
this.append(h('video', {controls: true}, h('source', {src: pathname})))
} else if (/\.(mp3|ogg)/i.test(pathname)) {
this.append(h('audio', {controls: true}, h('source', {src: pathname})))
} else {
let content = await self.readFile(pathname)
if (isEditing) {
// render editor
let textarea = h('textarea', {cls: 'editor'}, content)
this.append(textarea)
} else {
// render content
if (/\.(md|html)$/i.test(pathname)) {
if (pathname.endsWith('.md')) {
let md = new MarkdownIt()
content = md.render(content)
}
let contentEl = h('div', {cls: 'content'})
contentEl.innerHTML = content
this.append(contentEl)
} else {
this.append(h('pre', {cls: 'content'}, content))
}
}
}
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment