Renderer
The default render output is html
which includes client-side javascript
which hydrates a React application.
Static html
Interactive Components
Components (wiki link) alternativly regular link: Components
Viewers
Implementation
import path from 'path'
import remark2rehype from 'remark-rehype'
import rehype2react from 'rehype-react'
import {selectAll} from 'unist-util-select'
import vfile from 'vfile'
import hastCodeHandler from './utils/hast-util-code-handler'
import ReactDOMServer from 'react-dom/server'
import React from 'react'
import {processor as parserProcessor} from '../parser'
import {transcludeCode} from './transcludeCode'
import {extractPlugins} from './extractPlugins'
import Document from '../components/Document'
import Paragraph from '../components/base/Paragraph'
import Link from '../components/base/Link'
import {Codeblock} from '../components/base/Codeblock'
import Cell from '../components/Cell'
import { Section } from '../components/Section'
import { getConsoleForNamespace } from '../utils/console'
import { decorateLinkNode } from '../parser/links'
import {time} from '../utils/timings'
const timer = () => ({ns, marker}) => (t,f) => { time(ns,marker) }
const console = getConsoleForNamespace('renderer')
export function processor({fs, litroot, files, cwd, skipIncludes} = {}) {
console.log("Renderer: cwd", cwd)
let testGlobal = {}
return parserProcessor({fs, litroot, files, cwd, testGlobal})
.use(timer(),{ns:'renderer'})
// hoist ast to data
.use( (...args) => {
return (tree,file) => {
// console.log(`[${file.path}] Hoisting AST data to file.data.ast`)
file.data = file.data || {}
file.data.ast = tree
}
})
// transclude codeblocks with source
// when available
.use( transcludeCode, {fs} )
.use(timer(),{ns:'renderer', marker: 'transcludeCodeComplete'})
// includes and config
.use( ({fs, cwd, skipIncludes}) => {
return async (tree,file) => {
const includes = file?.data?.frontmatter?.includes || ['/config.lit']
let loaded = 0
if (skipIncludes || includes === "skip") {
console.log(`(${file.path}) Skipping includes.`)
return;
}
for (const include of includes) {
//const filepath = path.join(path.dirname(file.path), include)
const readPath = path.join(cwd || '', (include?.[0] !== '/' ? path.dirname(file.path) : ''), include)
console.log(`[${file.path}] [Include] Found include: "${include}" loading as: (${readPath})`)
// if (file.path === readPath) return
try {
const includeFile = await vfile({ path: readPath, contents: await fs.readFile(readPath, {encoding: 'utf8'}) })
const p = processor({fs, cwd, litroot, files, skipIncludes: true})
console.log(`[${file.path}] [Include] Constructed processor`)
const included = await p.process(includeFile)
console.log(`[${file.path}] [Include] Processed include: ${include}, had ${included.messages.length} messages.`, included.messages)
file.data = file.data || {}
file.data.plugins = Object.assign(file.data.plugins || {}, included.data.plugins || {})
file.messages = [...included.messages,...file.messages]
loaded += 1
} catch(err) {
console.error(`[${file.path}] Failed to load include: ${include}`, err)
}
}
console.log(`[${file.path}] Loaded ${loaded}/${includes.length} includes.`)
}
}, {fs, cwd, skipIncludes})
.use(timer(),{ns:'renderer', marker: 'includesComplete'})
// extract plugins
.use( extractPlugins, {testGlobal} )
.use(timer(),{ns:'renderer', marker: 'extractPluginsComplete'})
// extract files to data
.use( (...args) => {
return (tree,file) => {
// console.log(`[${file.path}] Extract codeblocks to file.data.files`)
file.data.files = selectAll("code", tree)
}
})
// hoist mdast data to hast data
// Disabled as failed to process due to JSON stringify error
.use( (...args) => {
return (tree,file) => {
// console.log(`[${file.path}] Hoist mdast data (disabled)`)
for (const code of selectAll("code", tree)) {
if (false && code.data) {
code.data.hProperties = code.data.hProperties || {}
code.data.hProperties.data = code.data
}
}
}
})
// use render plugins
.use( (...args) => {
return async (tree, file) => {
const rendererPlugins = file?.data?.plugins?.renderer || {}
// console.log(`[${file.path}] Looking for renderer plugins `)
for (const plugin in rendererPlugins) {
console.log(`[${file.path}] Render Plugin`, plugin)
await (rendererPlugins[plugin](...args))(tree, file)
}
}
}, {React, testGlobal})
.use(remark2rehype, {
allowDangerousHtml: true,
// passThrough: ['mdcode'],
handlers: {
code: hastCodeHandler,
},
})
.use(timer(),{ns:'renderer', marker: 'toRehypeComplete'})
.use(rehype2react, {
Fragment: React.Fragment,
allowDangerousHtml: true,
createElement: React.createElement,
passNode: true,
components: {
p: testGlobal.Paragraph || Paragraph,
a: testGlobal.Link || Link,
pre: testGlobal.Codeblock || Codeblock,
cell: testGlobal.Cell || Cell,
section: testGlobal.Section || Section
}
})
.use(timer(),{ns:'renderer', marker: 'toReactComplete'})
}
export async function renderedVFileToDoc(vfile, cmd) {
const root = path.resolve( cmd.output )
const dir = path.dirname( path.join(root, vfile.path) )
const relroot = path.relative(dir, root) || '.'
// console.log('Render to document vFile', vfile.path)
const notebook = <Document
file={vfile}
root={cmd.base || relroot}
backlinks={vfile.data.backlinks}
/>
vfile.contents = ReactDOMServer.renderToString(notebook)
vfile.extname = '.html'
return vfile
}
import path from 'path'
import { getConsoleForNamespace } from '../utils/console'
import { selectAll } from 'unist-util-select'
import {btoa, atob } from '../utils/safe-encoders'
import { transform } from '../repl'
const console = getConsoleForNamespace('plugins')
const extractModule = async (src, filename) => {
if (typeof global !== 'undefined'){
var Module = module.constructor;
var m = new Module();
if (typeof m._compile === 'function') {
const babel = transform(filename, src, {type: 'commonjs'})
m._compile(babel.code, filename);
console.log(`Compiled (${filename}) as commonjs module`, m, m.exports)
if (m.exports && Object.keys(m.exports).length) return m.exports;
else throw new Error("No module.exports when loaded as commonjs")
}
}
console.log(`Importing (${filename}) as es6 module via data:uri import.`)
// const blobUrl = URL.createObjectURL(new Blob([src], {type: 'text/javascript'}))
// return await import(/* webpackIgnore: true */ blobUrl)
const babel = transform(filename, src)
return await import(/* webpackIgnore: true */ `data:text/javascript;base64,${ btoa(babel.code)}`)
}
export const extractPlugins = ({fs} = {}) => {
return async (tree,file) => {
console.log("Checking for plugins")
file.data = file.data || {}
// file.data.plugins = {}
const blocks = selectAll("code", tree)
if (blocks?.length) await Promise.all(blocks.map(async block => {
const filename = (block.data
&& block.data.meta
&& block.data.meta.filename) || ''
// Generic plugins
if (block.data
&& block.data.meta
&& block.data.meta.directives
&& block.data.meta.directives.indexOf('disable') === -1
&& block.data.meta.directives.indexOf('plugin') >= 0) {
const meta = block.data.meta
const value = block.data?.value || block.value
console.log('Found Plugin', meta.raw)
let type = meta.type || 'unknown'
const types = ['parser', 'renderer', 'transformer', 'viewer', 'unknown', 'onsave', 'onload', 'onselect', 'menu', 'data', 'setting']
file.data = file.data || {}
file.data.plugins = file.data.plugins || {}
file.data.plugins[type] = file.data.plugins[type] || {}
if (meta.lang === 'css') {
const len = Object.keys(file.data.plugins[type]).length
const id = meta.of || meta.id || meta.filename || len
file.data.plugins[type][id] = {value: value}
return;
}
try {
let plugin = await extractModule(value, filename)
console.log("plugin module:", plugin)
let foundExport;
if (plugin?.asyncPlugin) {
plugin = await plugin.asyncPlugin()
}
for (const type of types) {
if (plugin?.[type]) {
foundExport = true
file.data.plugins[type] = file.data.plugins[type] || {}
const len = Object.keys(file.data.plugins[type]).length
const id = meta.of || meta.id || meta.filename || len
if (file.data.plugins[type] && file.data.plugins[type][id]) {
console.log(`Duplicate plugin for type: ${type} id: ${id}, overwriting.`)
}
file.data.plugins[type][id] = plugin[type]
}
}
if (types.indexOf(type) === -1 && plugin?.[type]) {
file.data.plugins[type] = file.data.plugins[type] || {}
const len = Object.keys(file.data.plugins[type]).length
const id = meta.of || meta.id || meta.filename || len
file.data.plugins[type][id] = plugin[type]
foundExport = true
}
if (!foundExport) throw new Error(`No plugin exported from module. for ${block.meta}`)
} catch(err) {
console.log("Failed to init plugin", meta.raw, err)
const msg = `Plugin Error (${type}): ` + (err.message || err.toString())
file.message(msg, block)
}
}
}))
}
}
import path from 'path'
import { getConsoleForNamespace } from '../utils/console'
import { selectAll } from 'unist-util-select'
const console = getConsoleForNamespace('transcludeCode')
export const transcludeCode = ({fs}) => {
return async (tree,file) => {
if(!fs) {
console.error("not enabled no fs.")
// return;
};
console.log(`(${file.path}) Checking for files to transclude`)
const blocks = selectAll("code", tree)
if (blocks?.length) await Promise.all(blocks.map( async block => {
const source = block?.data?.meta?.source
if (source) {
console.log(`(${file.path}) Found source to be transcluded`, block.data.meta.raw)
block.data.originalSource = block.value
block.data.hProperties.data = {originalSource: block.value}
if (source.uri) {
const resp = await fetch(source.uri)
if (resp.status >= 200 && resp.status < 400) {
const value = await resp.text()
// console.log("has value", value)
// block.value = value
block.data.value = value
} else {
const msg = `(${file.path}) Failed to load uri ` + block.data.meta.fromSource + " status: " + resp.status
file.message(msg, block)
console.error(msg)
}
}
else if (source.filename) {
const filePath = path.join(path.dirname(file.path), source.filename)
console.log(`(${file.path}) transclude "${source.filename}" as "${filePath}".`)
try {
const resp = await fs.readStat(filePath, {encoding: 'utf8'})
// console.log("has value", resp)
// block.value = resp.local.value || resp.remote.value
block.data.value = resp.local.value || resp.remote.value
} catch(err) {
const msg = `(${file.path}) Failed to load ` + block.data.meta.fromSource + " as " + filePath
file.message(msg, block)
console.error(msg, err)
}
}
}
}))
}
}