A list of the main components used to render a .lit
document.
Table of Contents
Document
import React, {useState} from 'react'
import path from 'path'
import App from './App'
import Backlinks from './Backlinks'
import { getConsoleForNamespace } from '../utils/console'
const console = getConsoleForNamespace('Document')
const Document = props => {
const files = props.files || []
const result = props.file.result
const title = props.file.data.frontmatter.title || props.file.stem
const theme = props.file.data.frontmatter.theme
return <html>
<head>
<title>{title}</title>
<meta name="litsrc" value={props.file.data.canonical}/>
<meta name="litroot" value={props.root}/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
{theme && <link rel="stylesheet" href={theme}/>}
<meta name="apple-mobile-web-app-capable" content="yes"/>
<link rel="apple-touch-icon" href={path.join(props.root, 'assets/lit-logo.png')}/>
<link rel="icon" href={path.join(props.root, 'assets/lit-logo.png')}/>
<link rel="stylesheet" href={path.join(props.root, 'style.css')}/>
</head>
<body>
<div id="lit-app"><App root={props.root} file={props.file} fs={props.fs} result={result} files={files} ssr={true}/></div>
<div id="backlinks"><Backlinks root={props.root} links={props.backlinks || []}/></div>
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<script async src={path.join(props.root, 'web.bundle.js')}/>
</body>
</html>
}
export default Document
Header
import React, { useState, useEffect } from 'react'
import * as clipboard from "clipboard-polyfill"
import source from 'unist-util-source'
import SelectionContext from './SelectionContext'
import { Identity } from '../utils/functions'
import { getConsoleForNamespace } from '../utils/console'
import { CloseIcon } from './Icons'
import { ErrorBoundary } from './ErrorBoundry'
import parser from '../parser'
import {version} from '../../package.json'
const console = getConsoleForNamespace('Header')
const setDebug = ev => {
ev.preventDefault()
const key = 'litDebug'
const example = 'All,fs,client,Cell,sections,etc...'
const storage = typeof localStorage !== 'undefined' && localStorage
const val = prompt("Set debug mask", storage.getItem(key) || example)
storage.setItem( key, val )
console.log(`Set ${key} to "${val}"`)
return false
}
const showInspector = ev => {
console.log('Show mobile inspector')
if (typeof eruda !== 'undefined' && eruda) eruda.show()
else alert("🚨 Eruda console not available")
}
const LED = ({color,status}) => {
return <span title={status} className={`led led-${color}`}></span>
}
const useHasMounted = () => {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
const Status = ({local, remote, sw, gh}) => {
const hasMounted = useHasMounted()
if (!hasMounted) return <>
<LED color={'grey'} title="Github" />
<LED color={'grey'} title="Service Worker" />
<LED color={'grey'} title="Status" />
</>;
const color = !hasMounted
? 'grey'
: (local && !remote)
? 'orange'
: (remote && !local)
? 'blue'
: (!remote && !local)
? 'red'
: 'green'
return <>
<LED color={gh ? 'green' : 'grey'} title="Github" />
<LED color={sw ? 'green' : 'grey'} title="Service Worker" />
<LED color={color} title="Status" />
</>
}
const Menu = props => {
// console.log('<Menu/>', props.title,)
const [open, setOpen] = useState(props.horizontal)
const toggleOpen = _ => setOpen(!open)
const disabled = props.disabled || (!props.onClick && !props.href && !props.children)
const handleClickTitle = ev => {
ev.stopPropagation()
if (disabled) return false
if (props.onClick) props.onClick()
else if (props.href) location.href = props.href
else toggleOpen()
return false
}
const catchClicks = ev => {
ev.stopPropagation()
if (!disabled && !props.horizontal) {
toggleOpen()
}
return false
}
const classes = [
props.horizontal ? 'horizontal' : null,
open ? 'open' : null,
props.children ? 'has-children' : null,
props.right ? 'right' : '',
].filter(Identity).join(' ')
return <menu className={classes} disabled={disabled} onClick={catchClicks}>
<li className={"MenuTitle"} key="menu-title" onClick={handleClickTitle}>
{ props.href
? <a href={props.href}>{props.title}</a>
: props.title }
</li>
{ !disabled && open && <li className="MenuItems">{ props.children }</li> }
</menu>
}
const Message = ({message, setSelectedCell}) => {
const scroll = ev => {
console.log('[Message] ', message)
setSelectedCell(message.location, true)
return false
}
const [hide, setHide] = useState(false);
const dismiss = () => setHide(true)
const [showAll, setShowAll] = useState(false)
const toggleShowAll = ev => {
setShowAll(!showAll)
return false
}
const classes = [
'lit-message',
showAll && 'showall',
].filter(Identity).join(' ')
return hide ? null : <div className={classes}>
<span className="message" onClick={toggleShowAll}>{message.message}</span>
<span className="name" onClick={scroll}>
{message.name.split(':').slice(1).join(':')}
</span>
<span className="close"><CloseIcon onClick={dismiss}/></span>
</div>
}
export const Header = ({ root, toggleViewSource, toggleModal}) => {
console.log('<Header/>')
const hasMounted = useHasMounted();
const ssr = !hasMounted
const [sw, setSw] = useState(null);
const resetFile = (ctx, localOnly) => async ev => {
const filepath = ctx.file.path
console.log("Reset File:", filepath)
const qualifier = localOnly ? "local" : "local And remote"
if (confirm(`Are you sure you want to delete the ${qualifier} copy of "${filepath}"`)) {
await ctx.fs.unlink(filepath, localOnly)
console.log("Deleted " + filepath + " reload page")
// location.reload()
}
}
const copyToClipboard = ctx => ev => {
clipboard.writeText(ctx.src)
console.log("Copied src to clipboard")
}
const ghToken = typeof localStorage !== 'undefined' && localStorage.getItem('ghToken')
const setGhToken = (ev) => {
localStorage.setItem('ghToken', prompt("GitHub personal access token"))
}
const copyCell = ctx => ev => {
const src = source(ctx.selectedCell,ctx.src)
clipboard.writeText(src)
console.log("Copied cell src to clipboard")
}
const deleteCell = ctx => ev => {
console.log('Deleting cell at pos:', ctx.selectedCell)
ctx.setSrc(ctx.selectedCell, '')
ctx.selectCell(null)
}
const cutCell = ctx => ev => {
console.log('Cutting cell at pos:', ctx.selectedCell)
const src = source(ctx.selectedCell,ctx.src)
clipboard.writeText(src)
ctx.setSrc(ctx.selectedCell, '')
ctx.selectCell(null)
}
const pasteAfterCell = ctx => async ev => {
console.log('Pasting after cell at pos:', ctx.selectedCell)
const src = source(ctx.selectedCell, ctx.src)
const add = await clipboard.readText()
ctx.setSrc(ctx.selectedCell, `${src}\n${add}`)
ctx.selectCell(null)
}
const addCodeCell = ctx => ev => {
console.log('Adding code cell after cell at pos:', ctx.selectedCell)
const src = source(ctx.selectedCell, ctx.src)
const add = "```js\n\n```"
ctx.setSrc(ctx.selectedCell, `${src}\n\n${add}`)
ctx.selectCell(null)
}
const clearCodeCell = ctx => ev => {
console.log('Clearing code cell at pos:', ctx.selectedCell)
const meta = source(ctx.selectedCell, ctx.src).split('\n')[0]
const end = '```'
ctx.setSrc(ctx.selectedCell, `${meta}\n${end}`)
}
const newFile = () => {
const filename = prompt('Please enter a file name or path')
if (filename) {
location.href = lit.parser.utils.links.resolver( filename ).href + '?title=' + encodeURIComponent(filename);
}
}
useEffect(async () => {
const resp = await fetch('--sw').then( res => res.json().catch( err=> null) ).catch(err => null)
setSw(resp)
}, [])
return <SelectionContext.Consumer>{(ctx) => {
const cellSelected = (ctx.selectedCell && ctx.selectedCell.start) || false
const local = ctx.file && ctx.file.data && ctx.file.data.times && ctx.file.data.times.local
const remote = ctx.file && ctx.file.data && ctx.file.data.times && ctx.file.data.times.remote
const ageMessage = ctx.file && ctx.file.data && ctx.file.data.times && ctx.file.data.times.ageMessage
const menuPlugins = !ssr && ctx?.file?.data?.plugins?.menu
const fileMenuPlugins = !ssr && ctx?.file?.data?.plugins?.["filemenu"]
const cellMenuPlugins = !ssr && ctx?.file?.data?.plugins?.["cellmenu"]
const sectionMenuPlugins = !ssr && ctx?.file?.data?.plugins?.["sectionmenu"]
const helpMenuPlugins = !ssr && ctx?.file?.data?.plugins?.["helpmenu"]
console.log('<Header/> plugins?', menuPlugins)
return <div id="lit-header">
<Menu title="Home" horizontal href={root}>
<Menu title="File" disabled={ssr}>
<span disabled className="meta">{ageMessage}</span>
<span onClick={newFile}>New</span>
<span disabled>Open</span>
<span disabled>Save</span>
<span onClick={toggleViewSource}>View Source</span>
<span onClick={copyToClipboard(ctx)}>Copy</span>
<span onClick={resetFile(ctx, true)}>Reset</span>
<span onClick={resetFile(ctx)}>Delete</span>
{ !fileMenuPlugins ? null : fileMenuPlugins && Object.keys(fileMenuPlugins).map( key => <ErrorBoundary>{fileMenuPlugins[key](ctx, {React, Menu, toggleModal})}</ErrorBoundary>) }
</Menu>
<Menu title="Cell" disabled={!cellSelected}>
<span disabled className="meta">
{cellSelected && `Lines ${ctx.selectedCell.start.line}-${ctx.selectedCell.end.line}`}
</span>
<span disabled={!cellSelected} onClick={addCodeCell(ctx)}>Add Code</span>
<span disabled={!cellSelected} onClick={deleteCell(ctx)}>Remove</span>
<span disabled={!cellSelected} onClick={clearCodeCell(ctx)}>Empty Code</span>
<span disabled>Edit</span>
<span disabled>Execute</span>
<Menu title="Move" disabled={!cellSelected}>
<span disabled>Up</span>
<span disabled>Down</span>
</Menu>
<span disabled={!cellSelected} onClick={copyCell(ctx)}>Copy</span>
<span disabled={!cellSelected} onClick={cutCell(ctx)}>Cut</span>
<span disabled={!cellSelected} onClick={pasteAfterCell(ctx)}>Paste After</span>
{ !cellMenuPlugins ? null : cellMenuPlugins && Object.keys(cellMenuPlugins).map( key => <ErrorBoundary>{cellMenuPlugins[key](ctx, {React, Menu , toggleModal})}</ErrorBoundary>) }
</Menu>
<Menu title="Section" disabled={!cellSelected}>
<span disabled>Collapse</span>
<span disabled>Remove</span>
<Menu title="Move">
<span disabled>Up</span>
<span disabled>Down</span>
</Menu>
{ !sectionMenuPlugins ? null : sectionMenuPlugins && Object.keys(sectionMenuPlugins).map( key => <ErrorBoundary>{sectionMenuPlugins[key](ctx, {React, Menu, toggleModal})}</ErrorBoundary>) }
</Menu>
{ !menuPlugins ? null : menuPlugins && Object.keys(menuPlugins).map( key => <ErrorBoundary>{menuPlugins[key](ctx, {React, Menu, toggleModal})}</ErrorBoundary>) }
<Menu title="Help" disabled={ssr}>
{ !helpMenuPlugins ? null : helpMenuPlugins && Object.keys(helpMenuPlugins).map( key => <ErrorBoundary>{helpMenuPlugins[key](ctx, {React, Menu, toggleModal})}</ErrorBoundary>) }
<a href="/config.html?file=config.lit">Config</a>
<Menu title="Debug">
<span onClick={setDebug}>Set Mask</span>
<span onClick={showInspector}>Show Inspector</span>
</Menu>
</Menu>
<Menu right title={<Status local={local} remote={remote} sw={sw} gh={ctx?.fs?.ghOrigin}/>}>
<span disabled>{`GitHub: ${ ctx?.fs?.ghOrigin ? 'Connected' : 'Not connected'}`}</span>
<span disabled>{`Service Worker: ${ sw ? sw.version : ' not'} active.`}</span>
{ctx.file && <span disabled>{`File: ${ctx.file.path}`}</span>}
{local && <span disabled>{`Local last updated ${local}`}</span> }
{remote && <span disabled>{`Remote last updated ${remote}`}</span> }
{ageMessage && <span disabled>{`Local is ${ageMessage} than remote.`}</span> }
{cellSelected && <span disabled>{`Lines ${ctx.selectedCell.start.line}-${ctx.selectedCell.end.line}`}</span> }
</Menu>
</Menu>
{!ssr && <div className="lit-messages">
{ ctx.file.messages.map( m => {
return <Message key={m.name} message={m} setSelectedCell={ctx.setSelectedCell} />
} ) }
</div> }
</div>
}}
</SelectionContext.Consumer>
}
App
import React, {useState, useEffect} from 'react'
import path from 'path'
import SelectionContext from './SelectionContext'
import { Header } from './Header'
import Editor from './Editor'
import Highlight from 'react-highlight.js'
import source from 'unist-util-source'
import patchSource from '../utils/unist-util-patch-source'
import { processor } from '../renderer'
import {utils as parserUtils} from '../parser'
import { getConsoleForNamespace } from '../utils/console'
import filter from 'unist-util-filter'
import { atPos } from '../utils/unist-util-select-position'
import { selectAll } from 'unist-util-select'
import { posstr } from '../utils/functions'
import { ErrorBoundary } from './ErrorBoundry'
const console = getConsoleForNamespace('App')
const {toMarkdown, ungroupSections} = parserUtils
const ONLOAD = "onload"
const ONSAVE = "onsave"
const ONSELECT = "onselect"
const onLifecyclePlugins = async (file, type, ...args) => {
const plugins = file?.data?.plugins?.[type] || {}
const keys = Object.keys(plugins)
console.log(`[${type}] plugins: ${keys.length}`)
for (const key of keys) {
if (typeof plugins[key] === 'function') {
await plugins[key](...args)
}
}
}
const ast2md = (ast) => {
const unGroup = ungroupSections()()
const tree = unGroup(ast)
const md = toMarkdown(tree)
return md
}
const App = ({root, file, fs, result, files, ssr}) => {
const [srcAndRes, setSrcAndRes] = useState({
src: file.contents.toString(),
res: result
})
const [res, setResult] = useState(result)
const [selectedCell, setSelectedCell] = useState(null)
const [viewSource, setViewSource] = useState(false)
const toggleViewSource = () => setViewSource(!viewSource)
const [modal, setModal] = useState(false)
const toggleModal = (val) => setModal(val)
const themePlugins = file?.data?.plugins?.theme
const themes = themePlugins && Object.keys(themePlugins).map(t=>({id: t, ...themePlugins[t]}))
const setSrcWrapper = async (pos, cellSource) => {
try {
console.log("<App/> Set src wrapper", posstr(pos.start), posstr(pos.end))
const patchedSrc = patchSource(srcAndRes.src, pos, cellSource.trimEnd())
if (patchedSrc === srcAndRes.src) {
console.log("No Change to source of document. Not updating.")
return;
}
file.contents = patchedSrc
file.messages = []
const processedFile = await processor({fs, files}).process(file)
console.log("Processed clientside on setSrc", file.path, processedFile)
await onLifecyclePlugins(processedFile, ONSAVE, patchedSrc, processedFile, processedFile.data.ast)
if (typeof window !== 'undefined') {
window.lit.file = processedFile
window.lit.ast = processedFile.data.ast
}
try {
await fs.writeFile(file.path, patchedSrc, {encoding: 'utf8'})
} catch (err) {
console.log("Failed to write file source to fs", file.path, err)
}
const tmpEnd = {line: pos.start.line + cellSource.split('\n').length }
const tmpPos = {start: pos.start, end: tmpEnd }
const tree = filter(processedFile.data.ast, atPos(tmpPos))
const nodes = selectAll('code', tree)
window._dss = {filter, selectAll, tree, ast: processedFile.data.ast, atPos, appliedAtPos: atPos(tmpPos), tmpPos}
console.log("[CodeCells in Change (pos)]", tmpPos, file.path, tree, nodes)
for (const codeCell of nodes) {
const meta = codeCell.data && codeCell.data.meta && codeCell.data.meta
const filename = meta && (meta.filename) //|| meta.source?.filename)
const extract = filename && (meta.isOutput) && meta.extract !== 'false'
const content = source(codeCell.position, patchedSrc).split('\n').slice(1,-1).join('\n')
if (extract) {
const filepath = path.join( path.dirname(file.path), filename)
await fs.writeFile(filepath, content)
console.log(`Wrote codefile ${filename} to "${filepath}" on disk`)
} else {
console.log(`Not writing code cell to fs`,filename, extract, codeCell)
}
}
setSrcAndRes({
src: patchedSrc,
res: processedFile.result
})
setSelectedCell(tmpPos)
} catch (err) {
console.log("failed to setSrc", pos, cellSource, err)
}
}
const setSelectedCellWrapper = async (pos, scroll) => {
console.log("Selected Cell:", pos)
await onLifecyclePlugins(window.lit.file, ONSELECT, pos, scroll)
setSelectedCell(pos)
if (pos && scroll) {
document.querySelector(`[startpos="${posstr(pos.start)}"]`).scrollIntoViewIfNeeded()
}
}
const state = {
fs: fs,
file: file,
src: srcAndRes.src,
selectedCell,
setSelectedCell: setSelectedCellWrapper,
setSrc: setSrcWrapper,
ast2md,
}
useEffect( async fn => {
await onLifecyclePlugins(file, ONLOAD, state)
},[])
console.log(`Render "${file.path}" (selected: ${selectedCell} `)
return <SelectionContext.Provider value={state}>
<ErrorBoundary>{ <Header root={root} toggleViewSource={toggleViewSource} toggleModal={toggleModal} ssr={ssr}/> }</ErrorBoundary>
{themes && themes.map( theme => {
return theme.url
? <link key={theme.id} rel="stylesheet" href={theme.url}/>
: <style key={theme.id} dangerouslySetInnerHTML={{__html: theme.value}}></style>
})}
<div id="content">
{ modal
? <ErrorBoundary>{modal}</ErrorBoundary>
: viewSource
? <ErrorBoundary><Highlight language="md">{srcAndRes.src}</Highlight></ErrorBoundary>
: <ErrorBoundary>{srcAndRes.res}</ErrorBoundary> }
</div>
</SelectionContext.Provider>
}
export default App
Selection Context
import React from 'react'
// SelectedCell is the hast node corresponding to the cell.
export default React.createContext({
fs: {},
file: {},
src: '',
selectedCell: null,
setSelectedCell: ()=>{},
setSrc: () => {},
});
Cell
import React, {useState,useEffect} from "react"
import vfile from 'vfile'
import path from 'path'
import source from 'unist-util-source'
import filter from 'unist-util-filter'
import { atPos } from '../utils/unist-util-select-position'
import { selectAll } from 'unist-util-select'
import CellMenu from './CellMenu'
import SelectionContext from './SelectionContext'
import Editor from './Editor'
import {Repl} from '../repl'
import {processor} from '../renderer'
import { getConsoleForNamespace } from '../utils/console'
import { posstr } from '../utils/functions'
const console = getConsoleForNamespace('Cell')
const childIs = (node, nodeType) => (node && node.children
&& node.children.length
&& node.children[0]
&& node.children[0].tagName === nodeType) ? node.children[0] : null
const Cell = props => {
const node = props.node
node.position = node.position || {}
const pos = node.position
const [src, setSrc] = useState('')
const [content, setContent] = useState(null)
const [loaded, setLoaded] = useState(null)
// const content = props.children
const [editing, setEditing] = useState(false)
const [executing, setExecuting] = useState(false)
const toggleEditing = () => setEditing(!editing)
const isSelected = ctx => {
return ctx.selectedCell
&& ctx.selectedCell.start && ctx.selectedCell.end
&& atPos(ctx.selectedCell)(node)
// && ctx.selectedCell.start.line === pos.start.line
// && ctx.selectedCell.end.line === pos.end.line
}
const toggleSelected = ctx => () => {
const selected = isSelected(ctx)
console.log(`Toggle selected (was ${selected})`, ctx.selectedCell)
ctx.setSelectedCell(selected ? null : pos)
}
const isCodeCell = childIs(props.node, 'pre')
const codeNode = childIs(isCodeCell, 'code');
const meta = codeNode ? codeNode.properties.meta : null
const codeSource = codeNode && codeNode.data && codeNode.data.value
const rawSource = codeSource && ("```" + (meta.raw || '') + "\n" + codeSource + "```")
const isTranscluded = meta?.source?.filename
const originalSource = meta && ("```" + (meta.raw || '') + "\n" + (isTranscluded ? (codeNode.data.originalSource||"") : codeNode.data.value) + "```")
const output = meta && meta.isOutput
const save = ctx => async args => {
console.log("Saving cell", pos)
const transform = meta && (meta.transformer || meta.lang)
const transformer = lit.file?.data?.plugins?.transformer?.[transform]
if (transformer) {
console.log("Transforming on save:", transformer)
const newSrc = await transformer({node, src, codeSource, rawSource, originalSource})
ctx.setSrc(pos, newSrc)
} else {
ctx.setSrc(pos, src)
}
setEditing(false)
}
const exec = ctx => async args => {
console.log('Executing cell', {pos, codeSource, rawSource, originalSource})
setExecuting(true)
const repl = meta.repl ? meta.repl : meta.lang
let result
let error
if (lit?.file?.data?.plugins?.repl?.[repl]) {
try {
result = {stdout: await lit.file.data.plugins.repl[repl](codeSource, meta, node) }
} catch(err) {
console.error("REPL plugin error", err)
error = true
result = err
}
} else {
try {
const repl = new Repl()
result = await repl.exec(codeSource, meta, node)
} catch(res) {
error = true
console.log('REPL promise rejected', res)
result = res
}
}
console.log('Execution result', result)
setExecuting(false)
if (result && meta.react && result.resp && React.isValidElement(result.resp))
setContent(result.resp)
else if (result && meta.selfmutate && typeof result.resp === "string") {
console.log("Experimental!! Special setSrc as cell is self mutating")
// assumes source has changed in the filesystem
// so re-render from that
if (ctx) ctx.setSrc(lit.ast.position, result.resp)
} else {
const outputMeta = (meta.hasOutput ? meta.output.raw : 'txt').trim() + (" attached=true updated=" + Date.now()) + (error ? ' !error' : '')
let output
if (ctx && meta?.output?.filename) {
const filepath = path.join( path.dirname(ctx.file.path), meta.output.filename)
console.log("Write repl output to file system ", filepath, result.stdout)
lit.fs.writeFile( filepath, result.stdout)
output = "\n```>"+ outputMeta.replace(meta.output.filename,'') + " < " + meta.output.filename + "\n\n```\n"
} else {
output = "\n```>"+ outputMeta + "\n" + result.stdout.replace(/\n```/g, "\n•••") + "\n```\n"
}
const src = isTranscluded ? originalSource : rawSource
console.log("exec setSrc", !!ctx, pos, src + output)
if (ctx) ctx.setSrc(pos, src + output)
else return src + output
}
}
useEffect( async () => {
if (!loaded && meta && meta.exec === 'onload') {
let result
try {
console.log("Onload execution: ", rawSource)
const output = await exec()()
console.log("produced output", output)
const outputVFile = await vfile({ path: meta.output?.filename || lit.location.src, contents: output})
result = await processor({fs: lit.fs,litroot: lit.location.root, disableExecOnLoad: true}).process(outputVFile)
console.log("Result", result)
setLoaded(true)
} catch(err) {
console.error("onload exec failed", err.message, err.stack)
return
}
const newContent = result.result.props.children[0].props.children[0].props.children;
console.log("setContent:", newContent)
setContent(newContent) // Whoa! That is a DirtyHack™️; result.result is a cell so will nest infinitely
}
},[])
const getClasses = ctx => [
isSelected(ctx) ? 'selected' : '',
editing ? 'editing' : '',
isCodeCell ? 'code' : '',
output ? 'output' : '',
executing ? 'executing' :'',
'cell'
].join(' ').trim() || undefined
const editCell = (src) => isCodeCell
? <div className="codeCellEditor">
<Editor src={src} update={setSrc}/>
</div>
: <Editor src={src} update={setSrc}/>
return <SelectionContext.Consumer>
{ ctx => {
const src = rawSource || source(pos, ctx.src)
return <div
onClick={toggleSelected(ctx)}
startpos={posstr(pos.start)}
endpos={posstr(pos.end)}
className={getClasses(ctx)}>
{ editing ? editCell(src) : <div className="cell-content">{content || props.children}</div> }
{ isSelected(ctx) && <CellMenu meta={meta} editing={editing} toggleEditing={toggleEditing} save={save(ctx)} exec={exec(ctx)}/>}
</div>
}}
</SelectionContext.Consumer>
}
export default Cell
Editor
import React from 'react'
import {EditorState, EditorView, basicSetup} from "@codemirror/basic-setup"
import {Compartment} from '@codemirror/state'
import {autocompletion} from "@codemirror/autocomplete"
// import {html} from "@codemirror/lang-html"
// import {oneDark} from "@codemirror/theme-one-dark"
//import {esLint} from "@codemirror/lang-javascript"
// @ts-ignore
//import Linter from "eslint4b-prebuilt"
//import {linter} from "@codemirror/lint"
//import {StreamLanguage} from "@codemirror/stream-parser"
//import {javascript} from "@codemirror/legacy-modes/mode/javascript"
const lineWrapping = new Compartment
export default class Editor extends React.Component {
constructor(props) {
super(props)
const {src, update} = props;
this.editorRef = React.createRef();
const linkOptions = lit.manifest.nodes.filter(n=>(n.exists && (n.id.endsWith('.lit') || n.id.endsWith('.md')))).map( n => {
const id = n.id.slice(1).replace(/\.lit|\.md$/, '')
return {
label: '[[' + id + ']]' ,
type: 'link',
detail: n.title,
apply: '[[' + id + '|' + n.title
}
})
this.editorState = window.cms = EditorState.create({
doc: props.src,
extensions: [
basicSetup,
EditorView.lineWrapping,
EditorView.updateListener.of(this.onUpdate.bind(this)),
autocompletion({
override: [function (context) {
let word = context.matchBefore(/\S*/)
console.log(word, context)
if(word.from == word.to && !context.explicit) return null
if (word.text.startsWith('[[')) {
return {
from: word.from,
options: [
// types: class, constant, enum, function, interface, keyword, method, namespace, property, text, type, and variable
...linkOptions
]
}
}
return {
from: word.from,
options: [
{label: "toc", type: "keyword", detail:"Table of contents", apply: "Table of contents"},
{label: "`", type: "variable", detail: "Fenced code block", apply:"```\n\n```"},
{label: "magic", type: "text", apply: "⠁⭒*.✩.*⭒⠁", detail: "macro"}
]
}
}],
activateOnTyping: true,
}),
// html(),
// oneDark
// linter(esLint(new Linter)),
// StreamLanguage.define(javascript),
]
})
}
onUpdate(viewUpdate) {
if (this.props.update && typeof this.props.update === 'function') {
this.props.update(viewUpdate.state.doc.toString())
}
}
componentDidMount() {
this.view = window.cmv = new EditorView({
state: this.editorState,
parent: this.editorRef.current
})
}
render() {
return <div className="editor" ref={this.editorRef}></div>
}
}
Code
Code Meta
import React from 'react'
import {Time} from './Time'
import { stringToHex, pickTextColorBasedOnBgColor } from '../utils/colors'
const colorStyle = (val) => {
let bgColor = stringToHex(val)
let textColor = pickTextColorBasedOnBgColor(bgColor, 'white', 'black')
// Custom exceptions
if (val === 'error') {
bgColor = 'red',
textColor = 'white'
}
return {
color: textColor,
backgroundColor: bgColor
}
}
export const CodeMeta = ({meta, toggleFullscreen, toggleLocalRemote, toggleCollapsed}) => {
return <span className="meta">
<span className="lang" onClick={toggleCollapsed}>{meta.lang}</span>
{meta.repl && <span className="repl">{meta.repl}</span> }
{meta.filename && <span className="filename">{meta.filename}</span>}
{meta.directives && meta.directives.map( (dir, i) => {
const onClick = dir === 'inline' ? toggleFullscreen : null
return <span key={dir} onClick={onClick} style={colorStyle(dir)} className={`directive dir-${dir}`}>{dir}</span>
})}
{ meta.attrs && Object.keys(meta.attrs).map(attr => {
const val = meta.attrs[attr]
// ignored attributes for display
if(val===true || val==="true" || attr==="updated" || attr==="repl") return null
return <span className={`attribute attr-${attr}`} key={attr} style={colorStyle(attr)}>{`${attr}=${val}`}</span>
})}
{meta.tags && meta.tags.map( (tag, i) => <span key={tag+i} style={colorStyle(tag)} className="tag">{tag}</span>)}
{meta.fromSource && <span onClick={toggleLocalRemote} className="source">{'< ' + meta.fromSource }</span> }
{meta.hasOutput && <span className="output">{'> ' + meta.output.raw}</span> }
{meta.updated && <span className="updatedAt">Updated <Time ms={parseInt(meta.updated)} /></span> }
</span>
}
Codeblock
import React, { useState } from 'react'
import {log, level} from '../../utils/console'
import { getConsoleForNamespace } from '../../utils/console'
import { DatesToRelativeDelta } from '../../utils/momento'
import Highlight from 'react-highlight.js'
import SelectionContext from '../SelectionContext'
import {getViewer} from '../Viewers'
import {CodeMeta} from '../CodeMeta'
import { Identity } from '../../utils/functions'
import { ErrorBoundary } from '../ErrorBoundry'
const console = getConsoleForNamespace('Codeblock')
const hasDirective = (meta, d) => {
return meta && meta.directives && meta.directives.length && meta.directives.indexOf(d) >= 0
}
export const Codeblock = props => {
const codeNode = props.node.children
&& props.node.children.length == 1
&& props.node.children[0].tagName === 'code'
? props.node.children[0]
: null;
const meta = codeNode ? codeNode.properties.meta : null
const dirs = (meta && meta.directives) || []
const tags = (meta && meta.tags) || []
const attrs = (meta && meta.attrs) || {}
const id = attrs.id || (meta && meta.filename)
const dirClasses = dirs.map(d=>'dir-'+d)
const tagClasses = tags.map(t=>'tag-'+t)
const hasDirective = (d) => {
return meta && meta.directives && meta.directives.length && meta.directives.indexOf(d) >= 0
}
const [localRemote, setLocalRemote] = useState('local')
const toggleLocalRemote = (ev) => {
ev.stopPropagation()
ev.preventDefault()
setLocalRemote(localRemote === 'local' ? 'remote' : 'local')
return false
}
const [fullScreen, setFullScreen] = useState(false)
const toggleFullscreen = (ev) => {
ev.stopPropagation()
ev.preventDefault()
setFullScreen(!fullScreen)
return false
}
const [collapsed, setCollapsed] = useState(hasDirective('collapse') ? 'collapsed' : '')
const toggleCollapsed = (ev) => {
ev.stopPropagation()
ev.preventDefault()
setCollapsed(collapsed === 'collapsed' ? 'uncollapsed' : 'collapsed')
return false
}
const anchorClick = ev => {
// add #id to history silently
history.replaceState(undefined, undefined, `#${id || ''}`)
}
return <SelectionContext.Consumer>
{ ctx => {
const Viewer = getViewer(meta, ctx.file.data && ctx.file.data.plugins && ctx.file.data.plugins.viewer && ctx.file.data.plugins.viewer)
const classes = [
...dirClasses,
...tagClasses,
meta && `lang-${meta.lang}`,
localRemote,
collapsed,
fullScreen && 'fullscreen',
'codecell',
meta && meta.isOutput && 'output',
].filter(Identity).join(' ')
if (codeNode) {
let source;
if (codeNode.data && codeNode.data.value) {
source = codeNode.data.value;
} else if (false && codeNode.children && codeNode.children[0]) {
source = codeNode.children[0].value
} else {
console.log('unknown source')
source = codeNode.value
}
codeNode.value = source
const above = Viewer && meta.directives && (meta.directives.indexOf('above') >= 0)
const below = Viewer && meta.directives && (meta.directives.indexOf('below') >= 0)
const highlighted = <Highlight language={(meta && meta.lang) || "plaintext"}>{source}</Highlight>
const metaView = meta && <CodeMeta meta={meta} toggleCollapsed={toggleCollapsed} toggleFullscreen={toggleFullscreen} toggleLocalRemote={toggleLocalRemote} />
return <div className={classes} onClick={anchorClick}>
{ id && <a name={id}/> }
{ meta && !above && metaView}
{ Viewer
? <ErrorBoundary>
{ below && highlighted }
<Viewer children={props.children} node={codeNode} React={React} fullscreen={fullScreen} file={ctx.file} lit={(typeof lit !== 'undefined' ? lit : {})}/>
</ErrorBoundary>
: meta && meta.isOutput
? <output>
{highlighted}
</output>
: (!above && !below)
? highlighted
: null }
{ meta && above && metaView }
{ above && highlighted }
</div>
} else {
console.log("Default codeblock", this.props.node.children[0])
return <div className="codecell"><pre className="default">{props.children}</pre></div>
}
}
}</SelectionContext.Consumer>
}
Link
import React from 'react'
import { ExternalLinkIcon, AnchorIcon } from '../Icons'
import { getConsoleForNamespace } from '../../utils/console'
const console = getConsoleForNamespace('Link')
const Link = props => {
const title = props.node.properties.title
const href = props.node.properties.href
const data = props.data || {}
const wikilink = props.wikilink ? 'true' : undefined
const fragment = data.fragment || href[0] === '#'
const local = !fragment && !data.external
const external = data.external || /^https?:\/\//.test(href)
const icon = external
? '↗'
: fragment
? '§'
: null
const classNames = [
props.className,
data.exists && 'exists',
local && 'local',
data.external && 'external',
fragment && 'fragment',
].filter(x=>x).join(' ')
const imgOnlyLink = props.node.children
&& props.node.children.length === 1
&& props.node.children[0].tagName === 'img'
return <a className={classNames}
{...props.node.properties}
// {...props.node.properties.data}
data={props.node.properties.data}
wikilink={wikilink}>
{props.children}
{icon && !imgOnlyLink && <span className="linkIcon">{icon}</span> }
</a>
}
export default Link
Backlinks
import React from 'react'
import path from 'path'
export default class Backlinks extends React.Component {
render() {
const links = this.props.links || []
if (!links.length) return null
const included = {}
const deduped = links.filter( l => {
if (!included[l.url]) {
included[l.url] = true
return true
}
})
return <>
<h4>{`Backlinks (${deduped.length})`}</h4>
<ol>
{deduped.map( (link) => {
return <li key={link.url}><a title={link.title} href={path.join(this.props.root, link.url)}>{link.title}</a></li>
})}
</ol>
</>
}
}