] to be flattened into
return dtdd
}
// Definition item inside a mixed list: wrap in …
return state.patch(node, {
type: 'element',
tagName: 'li',
properties: {},
children: [
{
type: 'element',
tagName: 'dl',
properties: {},
children: dtdd
}
]
})
}
return state.patch(node, {
type: 'element',
tagName: 'li',
properties: {},
children: state.all(node)
})
}
/**
* @param {import('../state.js').State} state
* @param {import('orga').ListItemCheckbox} node
* @returns {import('hast').Element}
*/
export function checkbox(state, node) {
return state.patch(node, {
type: 'element',
tagName: 'input',
properties: {
type: 'checkbox',
checked: node.checked,
disabled: true
},
children: []
})
}
================================================
FILE: packages/oast-to-hast/lib/handlers/newline.js
================================================
/**
* @param {import('../state.js').State} _state
* @param {import('orga').Newline} _node
* @param {import('orga').Parent | undefined} parent
* @returns {import('hast').Text | undefined}
*/
export function newline(_state, _node, parent) {
// In Org paragraphs, a single source newline is a soft break. For HTML
// output, normalize it to a space so text remains readable.
if (parent?.type === 'paragraph') {
return { type: 'text', value: ' ' }
}
return undefined
}
================================================
FILE: packages/oast-to-hast/lib/handlers/paragraph.js
================================================
/**
* Block-level HTML elements that are not allowed as descendants of .
* When a paragraph contains any of these, we must avoid wrapping in
.
*
* @see https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element
*/
const BLOCK_TAGS = new Set([
'address',
'article',
'aside',
'blockquote',
'details',
'dialog',
'dd',
'div',
'dl',
'dt',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hgroup',
'hr',
'li',
'main',
'nav',
'ol',
'p',
'pre',
'section',
'summary',
'table',
'ul'
])
/**
* @param {import('../state.js').State} state
* @param {import('orga').Paragraph} node
* @returns {import('hast').Element}
*/
export function paragraph(state, node) {
const properties = state.getAttrHtml(node)
const children = state.all(node)
// If any child is a block-level element (e.g. a from a media link
// with caption), wrapping in produces invalid HTML and causes hydration
// errors in React. Unwrap single block children; use
for mixed content.
const hasBlock = children.some(
(child) =>
child.type === 'element' &&
BLOCK_TAGS.has(/** @type {import('hast').Element} */ (child).tagName)
)
if (hasBlock) {
if (children.length === 1) {
return /** @type {import('hast').Element} */ (children[0])
}
return state.patch(node, {
type: 'element',
tagName: 'div',
properties: properties ?? {},
children
})
}
return state.patch(node, {
type: 'element',
tagName: 'p',
properties: properties ?? {},
children
})
}
================================================
FILE: packages/oast-to-hast/lib/handlers/section.js
================================================
/**
* @param {import('../state.js').State} state
* @param {import('orga').Section} node
* @returns {import('hast').Element|undefined}
*/
export function section(state, node) {
const headline = node.children.find((n) => n.type === 'headline')
if (headline) {
if (shouldSkip(state.options, headline.tags || [])) {
return undefined
}
}
let className = 'section'
const drawer = node.children.find((n) => n.type === 'drawer')
if (drawer && drawer.name === 'PROPERTIES') {
const lines = drawer.value.split('\n')
lines.forEach((line) => {
const m = line.match(/:(\w+):(.*)$/)
if (m && m[1].toUpperCase() === 'HTML_CONTAINER_CLASS') {
className = `${className} ${m[2].trim()}`
}
})
}
return state.patch(node, {
type: 'element',
tagName: 'div',
properties: { className: className.split(/\s+/).filter(Boolean) },
children: state.all(node)
})
}
/**
* @param {import('../state.js').Config} config
* @param {string[]} tags
* @returns {boolean}
*/
function shouldSkip({ selectTags = [], excludeTags = [] }, tags) {
if (selectTags.length > 0) {
return !tags.some((tag) => selectTags.includes(tag))
}
if (excludeTags.length > 0) {
return tags.some((tag) => excludeTags.includes(tag))
}
return false
}
================================================
FILE: packages/oast-to-hast/lib/handlers/table.js
================================================
/**
* @import {Element,ElementContent} from 'hast'
* @import {Table,TableCell,TableRow} from 'orga'
* @import {State} from '../state.js'
*/
/**
* @param {State} state
* @param {Table} node
* @returns {Element}
*/
export function table(state, node) {
const rows = state.all(node)
const hrIndex = rows.findIndex(
(row) => row.type === 'element' && row.tagName === 'hr'
)
/** @type {ElementContent[]} */
const headRows = []
/** @type {ElementContent[]} */
const bodyRows = []
rows.forEach((row, i) => {
if (i < hrIndex) {
headRows.push(row)
} else if (i > hrIndex) {
bodyRows.push(row)
}
})
/** @type {ElementContent[]} */
const tableContent = []
if (headRows.length > 0) {
tableContent.push({
type: 'element',
tagName: 'thead',
properties: {},
children: headRows
})
}
if (bodyRows.length > 0) {
tableContent.push({
type: 'element',
tagName: 'tbody',
properties: {},
children: bodyRows
})
}
const caption = node.attributes.caption
if (caption) {
tableContent.push({
type: 'element',
tagName: 'caption',
properties: {},
children: [{ type: 'text', value: `${caption}` }]
})
}
return state.patch(node, {
type: 'element',
tagName: 'table',
properties: {},
children: tableContent
})
}
/**
* @param {State} state
* @param {TableRow} node
* @returns {Element}
*/
export function row(state, node) {
return state.patch(node, {
type: 'element',
tagName: 'tr',
properties: {},
children: state.all(node)
})
}
/**
* @param {State} state
* @param {TableCell} node
* @returns {Element}
*/
export function cell(state, node) {
return state.patch(node, {
type: 'element',
tagName: 'td',
properties: {},
children: state.all(node)
})
}
/**
* @returns {Element}
*/
export function hr() {
return {
type: 'element',
tagName: 'hr',
properties: {},
children: []
}
}
================================================
FILE: packages/oast-to-hast/lib/handlers/text.js
================================================
/**
* @import {Element,Text} from 'hast'
*
*/
const wrapper = {
bold: 'strong',
italic: 'i',
code: 'code',
verbatim: 'code',
underline: 'u',
strikeThrough: 'del',
math: 'span'
}
/**
* @param {import('../state.js').State} state
* @param {import('orga').Text} node
* @returns {Element|Text}
*/
export function text(state, node) {
/** @type {Element|Text} */
let e = {
type: 'text',
value: node.value
}
if (node.style) {
e = {
type: 'element',
tagName: wrapper[node.style],
properties: {},
children: [e]
}
if (node.style === 'math') {
e.properties.className = ['math-inline']
}
}
return state.patch(node, e)
}
================================================
FILE: packages/oast-to-hast/lib/index.js
================================================
/**
* @typedef {import('hast').Nodes} HastNodes
* @typedef {import('orga').Nodes} OastNodes
* @typedef {Partial
| null | undefined} Options
*/
import { createState } from './state.js'
/**
* @param {OastNodes} tree
* oast tree.
* @param {Options} [options]
* Configuration (optional).
* @returns {HastNodes}
* hast tree.
*/
export function toHast(tree, options = {}) {
const state = createState(tree, options)
const node = state.one(tree)
if (Array.isArray(node)) {
return { type: 'root', children: node }
}
// if (tree.type === 'document') {
// return {
// type: 'root',
// data: tree.properties,
// children: node ? [node] : [],
// }
// }
return node || { type: 'root', children: [] }
}
================================================
FILE: packages/oast-to-hast/lib/state.js
================================================
/**
* @typedef {import('hast').Nodes} HastNodes
@import {ElementContent as HastElementContent,Root as HastRoot} from 'hast'
* @typedef {import('orga').Parent} OastParent
* @typedef {import('orga').Nodes} OastNodes
*/
/**
* @typedef {Object} Config
* @property {Handlers} handlers
* @property {string} linkTarget
* @property {(link: import('orga').Link) => string} linkHref
* @property {string[]} selectTags=[]
* @property {string[]} excludeTags=['noexport']
*/
/**
* @typedef {ReturnType} State
*
* @callback Handler
* @param {State} state
* @param {any} node
* @param {OastParent | undefined} parent
* @returns {Array | HastElementContent | HastRoot | undefined}
* hast node.
*
* @typedef {Partial>} Handlers
* Handle nodes.
*/
import { position } from 'unist-util-position'
import { handlers as defaultHandlers } from './handlers/index.js'
/**
* @param {OastNodes} _tree
* @param {Partial | null | undefined} [options = {}]
*/
export function createState(_tree, options = {}) {
/** @type {Handlers} */
let handlers = { ...defaultHandlers }
if (options?.handlers) {
handlers = { ...handlers, ...options.handlers }
}
const state = {
one,
all,
handlers,
getAttrHtml,
patch,
/** @type {Config} */
options: {
handlers,
linkTarget: '_self',
selectTags: [],
excludeTags: ['noexport'],
linkHref: defaultLinkHref,
...options
}
}
return state
/**
* @param {OastNodes} parent
* @returns {Array}
*/
function all(parent) {
/** @type {Array} */
const values = []
if ('children' in parent) {
parent.children.forEach((node) => {
const result = one(node, parent)
if (!result) {
return
}
if (Array.isArray(result)) {
values.push(...result)
} else {
// @ts-expect-error: can never be Root here
values.push(result)
}
})
}
return values
}
/**
* @param {OastNodes} node
* @param {OastParent | undefined} parent
*/
function one(node, parent = undefined) {
const handle = handlers[node.type] || unkownHandler
return handle(state, node, parent)
}
/**
* @param {OastNodes} node
* @returns {Record | undefined}
*/
function getAttrHtml(node) {
if ('properties' in node && 'attr_html' in node.properties) {
const a = node.properties.attr_html
if (typeof a === 'string') return
if (Array.isArray(a)) return
return a
}
}
/**
* @template {HastNodes} T
* @param {OastNodes} from
* @param {T} to
* @returns {T}
*/
function patch(from, to) {
if (from.position) {
to.position = position(from)
}
return to
}
}
/**
* @param {import('orga').Link} link
* @returns {string}
*/
function defaultLinkHref(link) {
const protocol = link.path.protocol
if (!protocol || protocol === 'internal' || protocol === 'file') {
return link.path.value
}
if (protocol === 'http' || protocol === 'https') {
return link.path.value
}
return `${protocol}:${link.path.value}`
}
/** @type {Handler} */
function unkownHandler(state, node) {
if (node?.children) {
return {
type: 'element',
tagName: 'div',
properties: {},
children: state.all(node)
}
} else if ('value' in node) {
return { type: 'text', value: `${node.value}` }
}
return undefined
}
================================================
FILE: packages/oast-to-hast/package.json
================================================
{
"name": "oast-to-hast",
"version": "4.5.3",
"description": "Transform OAST to HAST",
"files": [
"lib",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"exports": "./index.js",
"type": "module",
"author": "Xiaoxing Hu ",
"license": "MIT",
"repository": "https://github.com/orgapp/orgajs/tree/main/packages/oast-to-hast",
"dependencies": {
"hast-util-from-html": "^2.0.3",
"mime": "^3.0.0",
"orga": "workspace:^",
"parse5": "^7.1.2",
"unist-util-position": "^5.0.0"
},
"scripts": {
"test": "node --test tests/*.js"
},
"devDependencies": {
"@types/hast": "^3.0.4",
"@types/mime": "^3.0.1",
"@types/unist": "^3.0.3",
"hastscript": "^9.0.0"
}
}
================================================
FILE: packages/oast-to-hast/tests/block.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { h } from 'hastscript'
import { parse } from 'orga'
import { toHast } from '../index.js'
test('quote block preserves inline markup', () => {
const oast = parse(`#+BEGIN_QUOTE
Be *bold*, and if you must, be /italic/.
#+END_QUOTE
`)
const hast = removePosition(toHast(oast))
assert.deepEqual(hast, {
type: 'root',
data: {},
children: [
h('blockquote', [
'Be ',
h('strong', 'bold'),
', and if you must, be ',
h('i', 'italic'),
'.'
])
]
})
})
function removePosition(node) {
if (!node || typeof node !== 'object') return node
if (Array.isArray(node)) return node.map(removePosition)
const result = {}
for (const [key, value] of Object.entries(node)) {
if (key === 'position') continue
result[key] = removePosition(value)
}
return result
}
================================================
FILE: packages/oast-to-hast/tests/classname.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { parse } from 'orga'
import { toHast } from '../index.js'
test('src blocks emit code.className as token array', () => {
const oast = parse(`#+begin_src javascript
console.log('ok')
#+end_src`)
const hast = toHast(oast)
const code = hast.children[0]?.children?.[0]
assert.deepEqual(code?.tagName, 'code')
assert.deepEqual(code?.properties?.className, ['language-javascript'])
})
test('sections emit div.className as token array', () => {
const oast = parse(`* Heading
:PROPERTIES:
:HTML_CONTAINER_CLASS: foo bar
:END:
Body`)
const hast = toHast(oast)
const section = hast.children[0]
assert.deepEqual(section?.tagName, 'div')
assert.deepEqual(section?.properties?.className, ['section', 'foo', 'bar'])
})
================================================
FILE: packages/oast-to-hast/tests/heading.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { h } from 'hastscript'
import { toHast } from '../index.js'
test('heading', async () => {
const hast = toHast({
type: 'headline',
level: 3,
children: [{ type: 'text', value: 'Hello' }]
})
assert.deepEqual(hast, h('h3', 'Hello'))
})
================================================
FILE: packages/oast-to-hast/tests/list.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { h } from 'hastscript'
import { toHast } from '../index.js'
test('regular unordered list', () => {
const hast = toHast({
type: 'list',
ordered: false,
indent: 0,
children: [
{
type: 'list.item',
indent: 0,
children: [{ type: 'text', value: 'item one' }]
},
{
type: 'list.item',
indent: 0,
children: [{ type: 'text', value: 'item two' }]
}
]
})
assert.deepEqual(hast, h('ul', [h('li', 'item one'), h('li', 'item two')]))
})
test('pure definition list produces with flat / pairs (no wrapper)', () => {
const hast = toHast({
type: 'list',
ordered: false,
indent: 0,
children: [
{
type: 'list.item',
indent: 0,
tag: 'word',
children: [{ type: 'text', value: 'description.' }]
},
{
type: 'list.item',
indent: 0,
tag: 'another',
children: [{ type: 'text', value: 'definition.' }]
}
]
})
assert.deepEqual(
hast,
h('dl', [
h('dt', 'word'),
h('dd', 'description.'),
h('dt', 'another'),
h('dd', 'definition.')
])
)
})
test('pure definition list followed by a paragraph still produces
', () => {
// The parser appends a trailing newline node as a child of the list when
// followed by a blank line + paragraph. It must not be mistaken for a
// non-tagged item.
const hast = toHast({
type: 'list',
ordered: false,
indent: 0,
children: [
{
type: 'list.item',
indent: 0,
tag: 'hello',
children: [{ type: 'text', value: 'this is greeting.' }]
},
{
type: 'list.item',
indent: 0,
tag: 'world',
children: [{ type: 'text', value: 'this is the world.' }]
},
{ type: 'newline' }
]
})
assert.deepEqual(
hast,
h('dl', [
h('dt', 'hello'),
h('dd', 'this is greeting.'),
h('dt', 'world'),
h('dd', 'this is the world.')
])
)
})
test('mixed list wraps definition items as / ', () => {
const hast = toHast({
type: 'list',
ordered: false,
indent: 0,
children: [
{
type: 'list.item',
indent: 0,
children: [{ type: 'text', value: 'item one' }]
},
{
type: 'list.item',
indent: 0,
tag: 'word',
children: [{ type: 'text', value: 'definition goes here.' }]
}
]
})
assert.deepEqual(
hast,
h('ul', [
h('li', 'item one'),
h('li', [h('dl', [h('dt', 'word'), h('dd', 'definition goes here.')])])
])
)
})
================================================
FILE: packages/oast-to-hast/tests/paragraph.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { h } from 'hastscript'
import { toHast } from '../index.js'
test('paragraph with text', async () => {
const hast = toHast({
type: 'paragraph',
children: [{ type: 'text', value: 'Hello' }]
})
assert.deepEqual(hast, h('p', 'Hello'))
})
test('paragraph newline is rendered as a space', async () => {
const hast = toHast({
type: 'paragraph',
children: [
{ type: 'text', value: 'foo' },
{ type: 'newline' },
{ type: 'text', value: 'bar' }
]
})
assert.deepEqual(hast, h('p', ['foo', ' ', 'bar']))
})
test('paragraph mailto link keeps protocol in href', async () => {
const hast = toHast({
type: 'paragraph',
children: [
{
type: 'link',
path: { protocol: 'mailto', value: 'hi@unclex.net' },
attributes: {},
children: [{ type: 'text', value: 'send me an email' }]
}
]
})
assert.deepEqual(
hast,
h('p', [
h('a', { href: 'mailto:hi@unclex.net', target: '_self' }, [
'send me an email'
])
])
)
})
test('paragraph with single media figure unwraps to figure', async () => {
// A video link with a caption produces a
from the link handler.
// The paragraph handler must not wrap it in (invalid HTML).
const hast = toHast({
type: 'paragraph',
children: [
{
type: 'link',
path: { value: 'demo.mp4' },
attributes: { caption: 'A demo video' },
children: []
}
]
})
assert.deepEqual(
hast,
h('figure', [
h('video', { src: 'demo.mp4', controls: true }),
h('figcaption', 'A demo video')
])
)
})
test('paragraph with text and media figure wraps in div', async () => {
// Mixed content: text + block-level element → use
instead of
.
const hast = toHast({
type: 'paragraph',
children: [
{ type: 'text', value: 'Caption: ' },
{
type: 'link',
path: { value: 'demo.mp4' },
attributes: { caption: 'A demo video' },
children: []
}
]
})
assert.deepEqual(
hast,
h('div', [
{ type: 'text', value: 'Caption: ' },
h('figure', [
h('video', { src: 'demo.mp4', controls: true }),
h('figcaption', 'A demo video')
])
])
)
})
================================================
FILE: packages/oast-to-hast/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/oast-to-prose/CHANGELOG.md
================================================
# oast-to-prose
## 1.3.1
### Patch Changes
- bd2365a: fix types and linting
## 1.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
## 1.2.0
### Minor Changes
- d8861c2: update unified ecosystem
## 1.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
================================================
FILE: packages/oast-to-prose/index.js
================================================
/**
* @typedef {import('./lib/index.js').Options} Options
*/
export { schema, toProse } from './lib/index.js'
================================================
FILE: packages/oast-to-prose/lib/handlers/block.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Block} node
* @returns {ProseNode | Array | null | undefined}
*/
export function block(state, node) {
// TODO: handle more block types
state.ignore('block.begin', 'block.end')
const n = state.schema.node('code', null, state.all(node))
state.unignore('block.begin', 'block.end')
return n
}
================================================
FILE: packages/oast-to-prose/lib/handlers/headline.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Headline} node
* @returns {ProseNode | Array | null | undefined}
*/
export function headline(state, node) {
state.ignore('newline')
const n = state.schema.node(
'headline',
{ level: node.level },
state.all(node)
)
state.unignore('newline')
return n
}
================================================
FILE: packages/oast-to-prose/lib/handlers/index.js
================================================
import { block } from './block.js'
import { headline } from './headline.js'
import { link } from './link.js'
import { newline } from './newline.js'
import { paragraph } from './paragraph.js'
import { section } from './section.js'
import { stars } from './stars.js'
import { tags } from './tags.js'
import { text } from './text.js'
import { todo } from './todo.js'
/* @type {import('../index.js').Handlers} */
export const handlers = {
section,
text,
paragraph,
headline,
link,
block,
newline,
todo,
stars,
tags
}
export const ignore = ['link.path', 'emptyLine']
================================================
FILE: packages/oast-to-prose/lib/handlers/link.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Link} node
* @returns {ProseNode | Array | null | undefined}
*/
export function link(state, node) {
state.ignore('opening', 'closing')
const n = state.schema.node(
'link',
{
href: node.path.value
},
state.all(node)
)
state.unignore('opening', 'closing')
return n
}
================================================
FILE: packages/oast-to-prose/lib/handlers/newline.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @returns {ProseNode | Array | null | undefined}
*/
export function newline(state) {
return state.schema.node('newline')
}
================================================
FILE: packages/oast-to-prose/lib/handlers/paragraph.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Paragraph} node
* @returns {ProseNode | Array | null | undefined}
*/
export function paragraph(state, node) {
state.inParagraph = true
const n = state.schema.node('paragraph', null, state.all(node))
state.inParagraph = false
return n
}
================================================
FILE: packages/oast-to-prose/lib/handlers/section.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Section} node
* @returns {ProseNode | Array | null | undefined}
*/
export function section(state, node) {
const n = state.schema.node('section', null, state.all(node))
return n
}
================================================
FILE: packages/oast-to-prose/lib/handlers/stars.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Stars} node
* @returns {ProseNode | Array | null | undefined}
*/
export function stars(state, node) {
return state.schema.node('stars', {
level: node.level
})
}
================================================
FILE: packages/oast-to-prose/lib/handlers/tags.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Tags} node
* @returns {ProseNode | Array | null | undefined}
*/
export function tags(state, node) {
return node.tags.map((tag) =>
state.schema.node('tag', { tag }, state.schema.text(tag))
)
}
================================================
FILE: packages/oast-to-prose/lib/handlers/text.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Text} node
* @returns {ProseNode | Array | null | undefined}
*/
export function text(state, node) {
return state.schema.text(node.value)
}
================================================
FILE: packages/oast-to-prose/lib/handlers/todo.js
================================================
/**
* @typedef {import('prosemirror-model').Node} ProseNode
*/
/**
* @param {import('../state.js').State} state
* @param {import('orga').Todo} node
* @returns {ProseNode | Array | null | undefined}
*/
export function todo(state, node) {
return state.schema.node('todo', {
keyword: node.keyword,
actionable: node.actionable
})
}
================================================
FILE: packages/oast-to-prose/lib/index.js
================================================
/**
* @typedef {import('orga').Document} OastRoot
* @typedef {import('orga').Content} OastContent
* @typedef {import('orga').Parent} OastParent
* @typedef {import('prosemirror-model').Node} ProseNode
* @typedef {OastRoot | OastContent} OastNodes
*
* @callback Handler
* Handle a node.
* @param {import('./state.js').State} state
* Info passed around.
* @param {any} node
* oast node to handle.
* @param {OastParent | null | undefined} parent
* Parent of `node`.
* @returns {ProseNode | Array | null | undefined}
* prose node.
*
* @typedef {Record} Handlers
* Handle nodes.
*
* @typedef Options
* @property {import('prosemirror-model').Schema} schema
*/
import { defaultSchema } from './schema.js'
import { createParseState } from './state.js'
export const schema = defaultSchema
/**
* @param {OastRoot} tree
* @param {import('vfile').VFile} file
* @param {Options | undefined | null} [options]
* @returns {ProseNode}
*/
export function toProse(tree, file, options) {
const schema = options?.schema || defaultSchema
const state = createParseState(file, { schema })
const node = state.one(tree, null)
return schema.node('doc', null, node || undefined)
}
================================================
FILE: packages/oast-to-prose/lib/schema.js
================================================
import { Schema } from 'prosemirror-model'
export const defaultSchema = new Schema({
nodes: {
doc: {
content: 'block+'
},
section: {
content: 'headline block*',
group: 'block',
toDOM() {
return ['section', 0]
}
},
headline: {
attrs: { level: { default: 1 } },
content: 'inline*',
defining: true,
group: 'block',
// atom: true,
parseDOM: [
{ tag: 'h1', attrs: { level: 1 } },
{ tag: 'h2', attrs: { level: 2 } },
{ tag: 'h3', attrs: { level: 3 } },
{ tag: 'h4', attrs: { level: 4 } },
{ tag: 'h5', attrs: { level: 5 } },
{ tag: 'h6', attrs: { level: 6 } }
],
toDOM(node) {
return [`h${node.attrs.level}`, 0]
}
},
todo: {
group: 'inline',
inline: true,
attrs: {
keyword: {},
actionable: {}
},
toDOM(node) {
return [
'span',
{ class: 'org-todo', 'data-actionable': node.attrs.actionable },
node.attrs.keyword
]
}
},
stars: {
group: 'inline',
inline: true,
attrs: {
level: {}
},
toDOM(node) {
return [
'span',
{ class: 'org-stars', 'data-level': node.attrs.level },
'*'.repeat(node.attrs.level)
]
}
},
tag: {
content: 'text*',
group: 'inline',
inline: true,
toDOM() {
return ['span', { class: 'org-tag' }, 0]
}
},
paragraph: {
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM() {
return ['p', 0]
}
},
text: {
group: 'inline'
},
link: {
content: 'text*',
group: 'inline',
inline: true,
attrs: {
href: {},
title: { default: null }
},
inclusive: false,
parseDOM: [
{
tag: 'a[href]',
getAttrs(dom) {
if (typeof dom === 'string') return {}
return {
href: dom.getAttribute('href'),
title: dom.getAttribute('title')
}
}
}
],
toDOM(node) {
return ['a', node.attrs, 0]
}
},
code: {
content: 'inline*',
group: 'block',
attrs: { params: {} },
toDOM() {
return ['pre', ['code', 0]]
}
},
newline: {
inline: true,
group: 'inline',
selectable: false,
toDOM() {
return ['br']
}
}
},
marks: {
raw: {
attrs: {
type: {}
},
toDOM(node) {
return [
'code',
{
style: `border:1px solid red;before:${node.attrs.type}`,
'data-node-type': node.attrs.type
}
]
}
}
}
})
================================================
FILE: packages/oast-to-prose/lib/state.js
================================================
/**
* @typedef {import('orga').Document} OastRoot
* @typedef {import('orga').Content} OastContent
* @typedef {import('orga').Parent} OastParent
* @typedef {import('prosemirror-model').Node} ProseNode
* @typedef {OastRoot | OastContent} OastNodes
*
* @typedef HFields
* @property {import('prosemirror-model').Schema} schema
* Schema to use.
* @property {boolean} inParagraph
* Whether the current node is in a paragraph.
* @property {(node: OastNodes, parent: OastParent | null | undefined) => ProseNode | Array | null | undefined} one
* Transform an oast node to prose node.
* @property {(node: OastNodes) => Array} all
* Transform the children of an mdast parent to hast.
* @property {import('./index.js').Handlers} handlers
* Applied handlers.
* @property {(...types: string[]) => void} ignore
* @property {(...types: string[]) => void} unignore
* @property {string[]} ignored
*
* @typedef {HFields} State
*
* @typedef Options
* @property {import('prosemirror-model').Schema} schema
*/
import { handlers, ignore } from './handlers/index.js'
/**
* @param {import('vfile').VFile} file
* @param {Options} options
* @returns {State}
*/
export function createParseState(file, options) {
/** @type {State} */
const state = {
handlers: { ...handlers },
ignored: [...ignore],
schema: options.schema,
inParagraph: false,
ignore(...types) {
this.ignored.push(...types)
},
unignore(...types) {
this.ignored = this.ignored.filter((type) => !types.includes(type))
},
one(node, parent) {
if (this.ignored.includes(node.type)) {
return null
}
const handler = this.handlers[node.type]
if (handler) {
return handler(this, node, parent)
}
return defaultUnknownHandler(this, node, file)
},
all(parent) {
/** @type {Array} */
const values = []
if ('children' in parent) {
const nodes = parent.children
let index = -1
while (++index < nodes.length) {
const node = this.one(nodes[index], parent)
if (node) {
if (Array.isArray(node)) {
values.push(...node)
} else {
values.push(node)
}
}
}
}
return values
}
}
return state
}
/**
* Transform an unknown node.
*
* @param {State} state
* Info passed around.
* @param {OastNodes} node
* Unknown aast node.
* @param {import('vfile').VFile} file
* @returns {ProseNode | Array | null | undefined}
* Resulting pose node.
*/
function defaultUnknownHandler(state, node, file) {
console.log('unknown node', node)
if ('value' in node) {
const raw = state.schema.text(
getRawContent(node, file) || 'cannot find raw',
[state.schema.mark('raw', { type: node.type })]
)
if (!state.inParagraph) {
return state.schema.node('paragraph', null, [raw])
}
return raw
}
if ('children' in node) {
return state.all(node)
}
// return state.schema.node('error', null, [
// state.schema.text(`unknown node: ${node.type}`),
// ])
}
/**
* @param {OastNodes} node
* Unknown aast node.
* @param {import('vfile').VFile} file
* @returns {string | null}
*/
function getRawContent(node, file) {
if (node.position) {
const content = file.value.slice(
node.position.start.offset,
node.position.end.offset
)
if (typeof content !== 'string') {
throw new Error('content is not a string')
}
return content
}
return null
}
================================================
FILE: packages/oast-to-prose/package.json
================================================
{
"name": "oast-to-prose",
"version": "1.3.1",
"description": "Transform OAST to ProseMirror Document",
"author": "Xiaoxing Hu ",
"license": "MIT",
"type": "module",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/oast-to-prose#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/oast-to-prose"
},
"files": [
"lib",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"scripts": {},
"keywords": [],
"devDependencies": {
"orga": "workspace:^",
"vfile": "^6.0.3"
},
"dependencies": {
"prosemirror-model": "^1.19.3"
}
}
================================================
FILE: packages/oast-to-prose/tests/text.js
================================================
import * as assert from 'node:assert'
import test from 'node:test'
import { toProse } from '../lib/index.js'
test('test', () => {
const oast = {
type: 'document',
children: [
{
type: 'emptyLine'
},
{
type: 'section',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'hello world' },
{
type: 'newline'
},
{
type: 'emptyLine'
}
]
}
]
}
]
}
const prose = toProse(oast)
console.log({ prose })
assert.strictEqual(1, 1)
})
================================================
FILE: packages/oast-to-prose/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/orga/.projectile
================================================
================================================
FILE: packages/orga/CHANGELOG.md
================================================
# Change Log
## 4.7.1
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- text-kit@4.5.1
## 4.7.0
### Minor Changes
- 761c484: Preserve inline markup in quote and center blocks.
## 4.6.0
### Minor Changes
- a53cfea: all about the editor
This release improves the editor with new fold/shift/todo actions and settings, while also refactoring orga tokenization/parsing and lezer conversion to improve TODO handling, context hashing, and tree generation consistency.
### Patch Changes
- Updated dependencies [a53cfea]
- text-kit@4.5.0
## 4.5.1
### Patch Changes
- 60ad38f: migrate orga-build to be based on vite
## 4.5.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- text-kit@4.4.0
## 4.4.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- text-kit@4.3.0
## 4.3.0
### Minor Changes
- 7cfff79a: headline elements (stars, todo keywords and priority) end after the whitespaces
## 4.2.0
### Minor Changes
- ac322714: implement editor
### Patch Changes
- Updated dependencies [ac322714]
- text-kit@4.2.0
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- text-kit@4.1.0
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- text-kit@4.0.0
## 3.2.1
### Patch Changes
- eeccc870: - get image links out of paragraph
- some other minor fixes
- Updated dependencies [eeccc870]
- text-kit@3.0.2
## 3.2.0
### Minor Changes
- 6c1ddb9f: add latex support
## 3.1.5
### Patch Changes
- 4bde5155: tidy up dependencies
## 3.1.4
### Patch Changes
- ae83a3b0: - affiliated keyword support for list
- `HTML_CONTAINER_CLASS` support in properties drawer
- remove complex regex from inline parsing
## 3.1.3
### Patch Changes
- 09a3b5c6: fix planning position issue
## 3.1.2
### Patch Changes
- 594bf16b: ## @orgajs/orgx
Introducing new compiler `@orgajs/orgx`. It's a (almost) a direct port of [xdm](https://github.com/wooorm/xdm).
Most of the packages have already adopted `@orgajs/orgx`. The important ones are:
- `@orgajs/loader`
- `@orgajs/next`
- `gatsby-plugin-orga`
- `gatsby-theme-orga-docs`
- `@orgajs/playground'`
`gatsby-transformer-orga` is still using the original compiler, since it has it's own ecosystem which requires some work to do a proper migration. That means the derivative packages around it are using the original compiler.
- `gatsby-theme-orga-posts`
- `gatsby-theme-orga-posts-core`
## theme-ui support
`theme-ui` has `mdx` support builtin, and it's hard to do a clean extraction. So the package `@orgajs/theme-ui` is wrapping theme-ui, and provide orga specific tweaks. For gatsby, `gatsby-plugin-orga-theme-ui` is the equivalent of `gatsby-plugin-theme-ui`, but with orga support.
## 3.1.1
### Patch Changes
- 19156b8a: inject props into layout
## 3.1.0
### Minor Changes
- eeea0c54: introduce new token: empty line
## 3.0.1
### Patch Changes
- 6ed76057: # rename gatsby themes
- gatsby-theme-orga -> gatsby-theme-orga-posts-core
- gatsby-theme-blorg -> gatsby-theme-orga-posts
# add example projects
- gatsby-posts
- gatsby-posts-core
- 759e6149: # Bug Fixes
- fix lexer for parsing headline with todo keyword
- fix properties drawer issue
- fix orga-theme-ui-preset package
- fix gatsby-transformer-orga & gatsby-theme-blorg
# Improved Playground
- add `tokens` view
- show node type in tree views
- Updated dependencies [6ed76057]
- Updated dependencies [759e6149]
- text-kit@3.0.1
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
### Patch Changes
- Updated dependencies [8b02d10]
- text-kit@3.0.0
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- fix headline regex issue ([a36b75d](https://github.com/orgapp/orgajs/commit/a36b75d87da125f56edf7da1ddaf23771040ce1b))
- fix headline tags parsing issue [#126](https://github.com/orgapp/orgajs/issues/126) ([71d7f82](https://github.com/orgapp/orgajs/commit/71d7f8277708fc72d3b5be01ed0f72233bf7057b))
- fix phrasing content in headline ([31ca41c](https://github.com/orgapp/orgajs/commit/31ca41cb3b9b65a19dbc71a906f86ee4d725ad8f))
- inline markup check post ([d3d31c6](https://github.com/orgapp/orgajs/commit/d3d31c622dde2a2d469ac41884f2320497f811c6))
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
- **website:** better code block ([9d5b3a2](https://github.com/orgapp/orgajs/commit/9d5b3a2d554672d22523727e89b2b5c60dc6233d))
### Features
- add jsx support ([0d22499](https://github.com/orgapp/orgajs/commit/0d224990b412e064ebf6816608eea6766f93d60c))
- better code block in website ([3efe4cd](https://github.com/orgapp/orgajs/commit/3efe4cd96a63623e2f70028bd66346960ec90bec))
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
### Bug Fixes
- fix headline regex issue ([a36b75d](https://github.com/orgapp/orgajs/commit/a36b75d87da125f56edf7da1ddaf23771040ce1b))
- fix phrasing content in headline ([31ca41c](https://github.com/orgapp/orgajs/commit/31ca41cb3b9b65a19dbc71a906f86ee4d725ad8f))
- inline markup check post ([d3d31c6](https://github.com/orgapp/orgajs/commit/d3d31c622dde2a2d469ac41884f2320497f811c6))
### Features
- add jsx support ([0d22499](https://github.com/orgapp/orgajs/commit/0d224990b412e064ebf6816608eea6766f93d60c))
- better code block in website ([3efe4cd](https://github.com/orgapp/orgajs/commit/3efe4cd96a63623e2f70028bd66346960ec90bec))
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package orga
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package orga
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package orga
================================================
FILE: packages/orga/LICENSE.org
================================================
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/orga/README.org
================================================
* orga
Turns org-mode content into AST.
** Install
#+BEGIN_SRC sh
npm install --save orga
#+END_SRC
** Usage
#+BEGIN_SRC javascript
const { parse } = require('orga')
const ast = parse(`* TODO remember the milk :shopping:`)
#+END_SRC
~ast~ is an object looks like this:
#+BEGIN_SRC javascript
{
type: 'document',
properties: {},
children: [
{
type: 'section',
level: 1,
properties: {},
children: [
{
type: 'headline',
actionable: true,
content: 'remember the milk',
children: [
{
type: 'stars',
level: 1,
position: {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 }
},
parent: [Circular *1]
},
{
type: 'todo',
keyword: 'TODO',
actionable: true,
position: {
start: { line: 1, column: 3 },
end: { line: 1, column: 7 }
},
parent: [Circular *1]
},
{
type: 'text.plain',
value: 'remember the milk',
position: {
start: { line: 1, column: 8 },
end: { line: 1, column: 25 }
},
parent: [Circular *1]
},
{
type: 'tags',
tags: [ 'shopping' ],
position: {
start: { line: 1, column: 29 },
end: { line: 1, column: 39 }
},
parent: [Circular *1]
}
],
level: 1,
position: {
start: { line: 1, column: 1 },
end: { line: 1, column: 39 }
},
keyword: 'TODO',
tags: [ 'shopping' ],
parent: [Circular *2]
}
],
position: { start: { line: 1, column: 1 }, end: { line: 1, column: 39 } },
parent: [Circular *3]
}
],
position: { start: { line: 1, column: 1 }, end: { line: 1, column: 39 } }
}
#+END_SRC
================================================
FILE: packages/orga/package.json
================================================
{
"name": "orga",
"version": "4.7.1",
"description": "org-mode parser",
"files": [
"dist"
],
"main": "dist/index.js",
"type": "module",
"exports": {
".": "./dist/index.js",
"./todo": "./dist/todo.js"
},
"author": "Xiaoxing Hu ",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com:orgapp/orgajs.git",
"directory": "packages/orga"
},
"scripts": {
"clean": "tsc --build --clean",
"build": "tsc --build",
"test": "node --test --no-warnings --import tsx \"src/**/*.test.ts\""
},
"dependencies": {
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"text-kit": "workspace:^"
},
"devDependencies": {
"@types/node": "^25.3.2",
"@types/unist": "^3.0.3",
"tsx": "^4.19.2",
"typescript": "^5.9.2"
}
}
================================================
FILE: packages/orga/src/__tests__/block/affiliated keyword.json
================================================
{
"type": "document",
"properties": {
"nop": "code_block"
},
"children": [
{
"type": "keyword",
"key": "NAME",
"value": "code_block",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 19,
"offset": 18
}
},
"data": {
"hash": 36
}
},
{
"type": "block",
"name": "SRC",
"params": [
"javascript"
],
"value": "console.log('named code block')",
"attributes": {
"name": "code_block"
},
"children": [
{
"type": "text",
"value": "console.log('named code block')",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 42
},
"end": {
"line": 3,
"column": 32,
"offset": 73
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 19
},
"end": {
"line": 4,
"column": 10,
"offset": 83
}
},
"data": {
"hash": 3
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 84
},
"end": {
"line": 5,
"column": 1,
"offset": 84
}
},
"data": {
"hash": 18
}
},
{
"type": "keyword",
"key": "NAME",
"value": "code_block",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 85
},
"end": {
"line": 6,
"column": 19,
"offset": 103
}
},
"data": {
"hash": 36
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 104
},
"end": {
"line": 7,
"column": 1,
"offset": 104
}
},
"data": {
"hash": 18
}
},
{
"type": "block",
"name": "SRC",
"params": [
"javascript"
],
"value": "console.log('no name')",
"attributes": {},
"children": [
{
"type": "text",
"value": "console.log('no name')",
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 128
},
"end": {
"line": 9,
"column": 23,
"offset": 150
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 105
},
"end": {
"line": 10,
"column": 10,
"offset": 160
}
},
"data": {
"hash": 3
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 11,
"column": 1,
"offset": 161
},
"end": {
"line": 11,
"column": 1,
"offset": 161
}
},
"data": {
"hash": 18
}
},
{
"type": "keyword",
"key": "NOP",
"value": "code_block",
"position": {
"start": {
"line": 12,
"column": 1,
"offset": 162
},
"end": {
"line": 12,
"column": 18,
"offset": 179
}
},
"data": {
"hash": 36
}
},
{
"type": "block",
"name": "SRC",
"params": [
"javascript"
],
"value": "console.log('no name')",
"attributes": {},
"children": [
{
"type": "text",
"value": "console.log('no name')",
"position": {
"start": {
"line": 14,
"column": 1,
"offset": 203
},
"end": {
"line": 14,
"column": 23,
"offset": 225
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 13,
"column": 1,
"offset": 180
},
"end": {
"line": 15,
"column": 11,
"offset": 236
}
},
"data": {
"hash": 3
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 15,
"column": 11,
"offset": 236
}
}
}
================================================
FILE: packages/orga/src/__tests__/block/affiliated keyword.org
================================================
#+NAME: code_block
#+BEGIN_SRC javascript
console.log('named code block')
#+END_SRC
#+NAME: code_block
#+BEGIN_SRC javascript
console.log('no name')
#+END_SRC
#+NOP: code_block
#+BEGIN_SRC javascript
console.log('no name')
#+END_SRC
================================================
FILE: packages/orga/src/__tests__/block/export.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "html",
"value": "Hello ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 23,
"offset": 22
}
},
"data": {
"hash": 14
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 23
},
"end": {
"line": 2,
"column": 1,
"offset": 23
}
},
"data": {
"hash": 18
}
},
{
"type": "block",
"name": "EXPORT",
"params": [
"html"
],
"value": "world!
",
"attributes": {},
"children": [
{
"type": "text",
"value": "world!
",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 44
},
"end": {
"line": 4,
"column": 14,
"offset": 57
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 24
},
"end": {
"line": 5,
"column": 13,
"offset": 70
}
},
"data": {
"hash": 3
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 71
},
"end": {
"line": 6,
"column": 2,
"offset": 72
}
},
"data": {
"hash": 18
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 2,
"offset": 72
}
}
}
================================================
FILE: packages/orga/src/__tests__/block/export.org
================================================
#+HTML: Hello
#+BEGIN_EXPORT html
world!
#+END_EXPORT
================================================
FILE: packages/orga/src/__tests__/block/missing begin.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "console.log('hello')",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 21,
"offset": 20
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 21,
"offset": 20
},
"end": {
"line": 2,
"column": 1,
"offset": 21
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "console.log('world')",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 21
},
"end": {
"line": 2,
"column": 21,
"offset": 41
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 21,
"offset": 41
},
"end": {
"line": 3,
"column": 1,
"offset": 42
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 1,
"offset": 42
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 11,
"offset": 52
}
}
}
================================================
FILE: packages/orga/src/__tests__/block/missing begin.org
================================================
console.log('hello')
console.log('world')
#+END_SRC
================================================
FILE: packages/orga/src/__tests__/block/missing end.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "#+BEGIN_SRC javascript",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 23,
"offset": 22
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 23,
"offset": 22
},
"end": {
"line": 2,
"column": 1,
"offset": 23
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "console.log('hello')",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 23
},
"end": {
"line": 2,
"column": 21,
"offset": 43
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 21,
"offset": 43
},
"end": {
"line": 3,
"column": 1,
"offset": 44
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "console.log('world')",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 44
},
"end": {
"line": 3,
"column": 21,
"offset": 64
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 21,
"offset": 64
},
"end": {
"line": 3,
"column": 22,
"offset": 65
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 22,
"offset": 65
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 22,
"offset": 65
}
}
}
================================================
FILE: packages/orga/src/__tests__/block/missing end.org
================================================
#+BEGIN_SRC javascript
console.log('hello')
console.log('world')
================================================
FILE: packages/orga/src/__tests__/block/standard.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "block",
"name": "SRC",
"params": [
"javascript"
],
"value": "console.log('hello')\nconsole.log('world')",
"attributes": {},
"children": [
{
"type": "text",
"value": "console.log('hello')",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 23
},
"end": {
"line": 2,
"column": 21,
"offset": 43
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": "console.log('world')",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 44
},
"end": {
"line": 3,
"column": 21,
"offset": 64
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 11,
"offset": 75
}
},
"data": {
"hash": 3
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 11,
"offset": 75
}
}
}
================================================
FILE: packages/orga/src/__tests__/block/standard.org
================================================
#+BEGIN_SRC javascript
console.log('hello')
console.log('world')
#+END_SRC
================================================
FILE: packages/orga/src/__tests__/footnote/multiline.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "reference footnote ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 20,
"offset": 19
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 24,
"offset": 23
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 24,
"offset": 23
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 37
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 25,
"offset": 24
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"label": "1",
"data": {
"hash": 24
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 26
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 13
}
},
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 3,
"column": 7,
"offset": 33
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 3,
"column": 32,
"offset": 58
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 32,
"offset": 58
},
"end": {
"line": 4,
"column": 1,
"offset": 59
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "And here is ",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 59
},
"end": {
"line": 4,
"column": 13,
"offset": 71
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "bold",
"value": "another",
"position": {
"start": {
"line": 4,
"column": 13,
"offset": 71
},
"end": {
"line": 4,
"column": 22,
"offset": 80
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " line.",
"position": {
"start": {
"line": 4,
"column": 22,
"offset": 80
},
"end": {
"line": 4,
"column": 28,
"offset": 86
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 28,
"offset": 86
},
"end": {
"line": 4,
"column": 29,
"offset": 87
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 4,
"column": 29,
"offset": 87
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 4,
"column": 29,
"offset": 87
}
},
"data": {
"hash": 2
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 29,
"offset": 87
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/multiline.org
================================================
reference footnote [fn:1]
[fn:1] Content of the footnote.
And here is *another* line.
================================================
FILE: packages/orga/src/__tests__/footnote/standard.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "reference footnote ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 20,
"offset": 19
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 24,
"offset": 23
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 24,
"offset": 23
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 37
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 25,
"offset": 24
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"label": "1",
"data": {
"hash": 24
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 26
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 13
}
},
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 3,
"column": 7,
"offset": 33
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 3,
"column": 32,
"offset": 58
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 32,
"offset": 58
},
"end": {
"line": 3,
"column": 33,
"offset": 59
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 3,
"column": 33,
"offset": 59
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 3,
"column": 33,
"offset": 59
}
},
"data": {
"hash": 2
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 33,
"offset": 59
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/standard.org
================================================
reference footnote [fn:1]
[fn:1] Content of the footnote.
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by empty lines.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 7,
"offset": 6
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 2,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "And here is another line.",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 32
},
"end": {
"line": 2,
"column": 26,
"offset": 57
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 26,
"offset": 57
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 13
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "still belongs to fn:1",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 59
},
"end": {
"line": 4,
"column": 22,
"offset": 80
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 22,
"offset": 80
},
"end": {
"line": 5,
"column": 1,
"offset": 81
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 81
},
"end": {
"line": 5,
"column": 1,
"offset": 81
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 59
},
"end": {
"line": 5,
"column": 1,
"offset": 81
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 1,
"offset": 82
}
},
"data": {
"hash": 2
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 82
},
"end": {
"line": 6,
"column": 1,
"offset": 82
}
},
"data": {
"hash": 18
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "This is not.",
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 83
},
"end": {
"line": 7,
"column": 13,
"offset": 95
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 7,
"column": 13,
"offset": 95
},
"end": {
"line": 7,
"column": 14,
"offset": 96
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 83
},
"end": {
"line": 7,
"column": 14,
"offset": 96
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 7,
"column": 14,
"offset": 96
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by empty lines.org
================================================
[fn:1] Content of the footnote.
And here is another line.
still belongs to fn:1
This is not.
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by footnote.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 7,
"offset": 6
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 2,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "And here is another line.",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 32
},
"end": {
"line": 2,
"column": 26,
"offset": 57
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 26,
"offset": 57
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 2
}
},
{
"type": "footnote",
"label": "2",
"children": [
{
"type": "footnote.label",
"label": "2",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 7,
"offset": 64
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "another footnote.",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 65
},
"end": {
"line": 3,
"column": 25,
"offset": 82
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 25,
"offset": 82
},
"end": {
"line": 3,
"column": 26,
"offset": 83
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 65
},
"end": {
"line": 3,
"column": 26,
"offset": 83
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 26,
"offset": 83
}
},
"data": {
"hash": 2
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 26,
"offset": 83
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by footnote.org
================================================
[fn:1] Content of the footnote.
And here is another line.
[fn:2] another footnote.
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by headline.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 7,
"offset": 6
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 2,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "And here is another line.",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 32
},
"end": {
"line": 2,
"column": 26,
"offset": 57
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 26,
"offset": 57
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 2
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 2,
"offset": 59
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "A Headline",
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 60
},
"end": {
"line": 3,
"column": 13,
"offset": 70
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 14,
"offset": 71
}
},
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 58
},
"end": {
"line": 3,
"column": 14,
"offset": 71
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 14,
"offset": 71
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/stopped by headline.org
================================================
[fn:1] Content of the footnote.
And here is another line.
* A Headline
================================================
FILE: packages/orga/src/__tests__/footnote/with block.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "reference footnote ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 20,
"offset": 19
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 24,
"offset": 23
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 24,
"offset": 23
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 37
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 25,
"offset": 24
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"label": "1",
"data": {
"hash": 24
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 26
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 13
}
},
{
"type": "footnote",
"label": "1",
"children": [
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 3,
"column": 7,
"offset": 33
}
},
"data": {
"hash": 37
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Content of the footnote.",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 3,
"column": 32,
"offset": 58
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 32,
"offset": 58
},
"end": {
"line": 4,
"column": 1,
"offset": 59
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 34
},
"end": {
"line": 4,
"column": 1,
"offset": 59
}
},
"data": {
"hash": 13
}
},
{
"type": "block",
"name": "SRC",
"params": [
"javascript"
],
"value": "console.log('footnote with code block')",
"attributes": {},
"children": [
{
"type": "text",
"value": "console.log('footnote with code block')",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 82
},
"end": {
"line": 5,
"column": 40,
"offset": 121
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 59
},
"end": {
"line": 6,
"column": 11,
"offset": 132
}
},
"data": {
"hash": 3
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 27
},
"end": {
"line": 6,
"column": 11,
"offset": 132
}
},
"data": {
"hash": 2
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 11,
"offset": 132
}
}
}
================================================
FILE: packages/orga/src/__tests__/footnote/with block.org
================================================
reference footnote [fn:1]
[fn:1] Content of the footnote.
#+BEGIN_SRC javascript
console.log('footnote with code block')
#+END_SRC
================================================
FILE: packages/orga/src/__tests__/headline/broken drawer.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 11
}
},
"data": {
"hash": 12
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": ":PROPERTIES:",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 11
},
"end": {
"line": 2,
"column": 13,
"offset": 23
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 13,
"offset": 23
},
"end": {
"line": 3,
"column": 1,
"offset": 24
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "key1: value1",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 24
},
"end": {
"line": 3,
"column": 13,
"offset": 36
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 13,
"offset": 36
},
"end": {
"line": 4,
"column": 1,
"offset": 37
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "key2: value2",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 37
},
"end": {
"line": 4,
"column": 13,
"offset": 49
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 13,
"offset": 49
},
"end": {
"line": 5,
"column": 1,
"offset": 50
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 50
},
"end": {
"line": 5,
"column": 1,
"offset": 50
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 11
},
"end": {
"line": 5,
"column": 1,
"offset": 50
}
},
"data": {
"hash": 13
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Paragraph",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 51
},
"end": {
"line": 6,
"column": 10,
"offset": 60
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 6,
"column": 10,
"offset": 60
},
"end": {
"line": 6,
"column": 11,
"offset": 61
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 51
},
"end": {
"line": 6,
"column": 11,
"offset": 61
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 11,
"offset": 61
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 11,
"offset": 61
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/broken drawer.org
================================================
* headline
:PROPERTIES:
key1: value1
key2: value2
Paragraph
================================================
FILE: packages/orga/src/__tests__/headline/drawers.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "section",
"level": 1,
"properties": {
"category": "test",
"effort": "5"
},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline with properties",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 27,
"offset": 26
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 27
}
},
"data": {
"hash": 12
}
},
{
"type": "drawer",
"name": "PROPERTIES",
"value": "\n:CATEGORY: test\n:Effort: 5\n",
"children": [
{
"type": "drawer.begin",
"name": "PROPERTIES",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 27
},
"end": {
"line": 2,
"column": 13,
"offset": 39
}
},
"data": {
"hash": 31
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 13,
"offset": 39
},
"end": {
"line": 3,
"column": 1,
"offset": 40
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": ":CATEGORY: test",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 40
},
"end": {
"line": 3,
"column": 16,
"offset": 55
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 16,
"offset": 55
},
"end": {
"line": 4,
"column": 1,
"offset": 56
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": ":Effort: 5",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 56
},
"end": {
"line": 4,
"column": 13,
"offset": 68
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 13,
"offset": 68
},
"end": {
"line": 5,
"column": 1,
"offset": 69
}
},
"data": {
"hash": 17
}
},
{
"type": "drawer.end",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 69
},
"end": {
"line": 5,
"column": 6,
"offset": 74
}
},
"data": {
"hash": 32
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 27
},
"end": {
"line": 5,
"column": 6,
"offset": 74
}
},
"data": {
"hash": 5
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 75
},
"end": {
"line": 6,
"column": 1,
"offset": 75
}
},
"data": {
"hash": 18
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 7,
"column": 1,
"offset": 76
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 76
},
"end": {
"line": 7,
"column": 2,
"offset": 77
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline with drawer",
"position": {
"start": {
"line": 7,
"column": 3,
"offset": 78
},
"end": {
"line": 7,
"column": 23,
"offset": 98
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 76
},
"end": {
"line": 8,
"column": 1,
"offset": 99
}
},
"data": {
"hash": 12
}
},
{
"type": "drawer",
"name": "LOG",
"value": "\nhere are\nsome log\n",
"children": [
{
"type": "drawer.begin",
"name": "LOG",
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 99
},
"end": {
"line": 8,
"column": 6,
"offset": 104
}
},
"data": {
"hash": 31
}
},
{
"type": "newline",
"position": {
"start": {
"line": 8,
"column": 6,
"offset": 104
},
"end": {
"line": 9,
"column": 1,
"offset": 105
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "here are",
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 105
},
"end": {
"line": 9,
"column": 9,
"offset": 113
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 9,
"column": 9,
"offset": 113
},
"end": {
"line": 10,
"column": 1,
"offset": 114
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "some log",
"position": {
"start": {
"line": 10,
"column": 1,
"offset": 114
},
"end": {
"line": 10,
"column": 9,
"offset": 122
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 10,
"column": 9,
"offset": 122
},
"end": {
"line": 11,
"column": 1,
"offset": 123
}
},
"data": {
"hash": 17
}
},
{
"type": "drawer.end",
"position": {
"start": {
"line": 11,
"column": 1,
"offset": 123
},
"end": {
"line": 11,
"column": 6,
"offset": 128
}
},
"data": {
"hash": 32
}
}
],
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 99
},
"end": {
"line": 11,
"column": 6,
"offset": 128
}
},
"data": {
"hash": 5
}
}
],
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 76
},
"end": {
"line": 11,
"column": 7,
"offset": 129
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 11,
"column": 7,
"offset": 129
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/drawers.org
================================================
* headline with properties
:PROPERTIES:
:CATEGORY: test
:Effort: 5
:END:
* headline with drawer
:LOG:
here are
some log
:END:
================================================
FILE: packages/orga/src/__tests__/headline/keyword.json
================================================
{
"type": "document",
"properties": {
"todo": "TODO NEXT | DONE"
},
"children": [
{
"type": "keyword",
"key": "TODO",
"value": "TODO NEXT | DONE",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 36
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 25
},
"end": {
"line": 2,
"column": 1,
"offset": 25
}
},
"data": {
"hash": 18
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": true,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 3,
"column": 2,
"offset": 27
}
},
"data": {
"hash": 25
}
},
{
"type": "todo",
"keyword": "TODO",
"actionable": true,
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 28
},
"end": {
"line": 3,
"column": 7,
"offset": 32
}
},
"data": {
"hash": 26
}
},
{
"type": "text",
"value": "with keyword",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 33
},
"end": {
"line": 3,
"column": 20,
"offset": 45
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 4,
"column": 1,
"offset": 46
}
},
"keyword": "TODO",
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 4,
"column": 1,
"offset": 46
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": true,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 46
},
"end": {
"line": 4,
"column": 2,
"offset": 47
}
},
"data": {
"hash": 25
}
},
{
"type": "todo",
"keyword": "NEXT",
"actionable": true,
"position": {
"start": {
"line": 4,
"column": 3,
"offset": 48
},
"end": {
"line": 4,
"column": 7,
"offset": 52
}
},
"data": {
"hash": 26
}
},
{
"type": "text",
"value": "with keyword",
"position": {
"start": {
"line": 4,
"column": 8,
"offset": 53
},
"end": {
"line": 4,
"column": 20,
"offset": 65
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 46
},
"end": {
"line": 5,
"column": 1,
"offset": 66
}
},
"keyword": "NEXT",
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 46
},
"end": {
"line": 5,
"column": 1,
"offset": 66
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 66
},
"end": {
"line": 5,
"column": 2,
"offset": 67
}
},
"data": {
"hash": 25
}
},
{
"type": "todo",
"keyword": "DONE",
"actionable": false,
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 68
},
"end": {
"line": 5,
"column": 7,
"offset": 72
}
},
"data": {
"hash": 26
}
},
{
"type": "text",
"value": "with keyword",
"position": {
"start": {
"line": 5,
"column": 8,
"offset": 73
},
"end": {
"line": 5,
"column": 20,
"offset": 85
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 66
},
"end": {
"line": 5,
"column": 21,
"offset": 86
}
},
"keyword": "DONE",
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 66
},
"end": {
"line": 5,
"column": 21,
"offset": 86
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 21,
"offset": 86
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/keyword.org
================================================
#+TODO: TODO NEXT | DONE
* TODO with keyword
* NEXT with keyword
* DONE with keyword
================================================
FILE: packages/orga/src/__tests__/headline/nested.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 1",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 13,
"offset": 12
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 13
}
},
"data": {
"hash": 12
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Paragraph",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 13
},
"end": {
"line": 2,
"column": 10,
"offset": 22
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 10,
"offset": 22
},
"end": {
"line": 3,
"column": 1,
"offset": 23
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 13
},
"end": {
"line": 3,
"column": 1,
"offset": 23
}
},
"data": {
"hash": 13
}
},
{
"type": "section",
"level": 2,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 2,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 23
},
"end": {
"line": 3,
"column": 3,
"offset": 25
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 1.1",
"position": {
"start": {
"line": 3,
"column": 4,
"offset": 26
},
"end": {
"line": 3,
"column": 16,
"offset": 38
}
},
"data": {
"hash": 19
}
}
],
"level": 2,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 23
},
"end": {
"line": 4,
"column": 1,
"offset": 39
}
},
"data": {
"hash": 12
}
},
{
"type": "section",
"level": 3,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 3,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 39
},
"end": {
"line": 4,
"column": 4,
"offset": 42
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 1.1.1",
"position": {
"start": {
"line": 4,
"column": 5,
"offset": 43
},
"end": {
"line": 4,
"column": 19,
"offset": 57
}
},
"data": {
"hash": 19
}
}
],
"level": 3,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 39
},
"end": {
"line": 5,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 12
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "content",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 58
},
"end": {
"line": 5,
"column": 8,
"offset": 65
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 5,
"column": 8,
"offset": 65
},
"end": {
"line": 6,
"column": 1,
"offset": 66
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 66
},
"end": {
"line": 6,
"column": 1,
"offset": 66
}
},
"data": {
"hash": 18
}
}
],
"attributes": {},
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 58
},
"end": {
"line": 6,
"column": 1,
"offset": 66
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 39
},
"end": {
"line": 7,
"column": 1,
"offset": 67
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 23
},
"end": {
"line": 7,
"column": 1,
"offset": 67
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 2,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 2,
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 67
},
"end": {
"line": 7,
"column": 3,
"offset": 69
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 1.2",
"position": {
"start": {
"line": 7,
"column": 4,
"offset": 70
},
"end": {
"line": 7,
"column": 16,
"offset": 82
}
},
"data": {
"hash": 19
}
}
],
"level": 2,
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 67
},
"end": {
"line": 8,
"column": 1,
"offset": 83
}
},
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 7,
"column": 1,
"offset": 67
},
"end": {
"line": 8,
"column": 1,
"offset": 83
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 8,
"column": 1,
"offset": 83
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 83
},
"end": {
"line": 8,
"column": 2,
"offset": 84
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 2",
"position": {
"start": {
"line": 8,
"column": 3,
"offset": 85
},
"end": {
"line": 8,
"column": 13,
"offset": 95
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 83
},
"end": {
"line": 9,
"column": 1,
"offset": 96
}
},
"data": {
"hash": 12
}
},
{
"type": "section",
"level": 2,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 2,
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 96
},
"end": {
"line": 9,
"column": 3,
"offset": 98
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline 2.2",
"position": {
"start": {
"line": 9,
"column": 4,
"offset": 99
},
"end": {
"line": 9,
"column": 16,
"offset": 111
}
},
"data": {
"hash": 19
}
}
],
"level": 2,
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 96
},
"end": {
"line": 9,
"column": 17,
"offset": 112
}
},
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 96
},
"end": {
"line": 9,
"column": 17,
"offset": 112
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 83
},
"end": {
"line": 9,
"column": 17,
"offset": 112
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 9,
"column": 17,
"offset": 112
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/nested.org
================================================
* headline 1
Paragraph
** headline 1.1
*** headline 1.1.1
content
** headline 1.2
* headline 2
** headline 2.2
================================================
FILE: packages/orga/src/__tests__/headline/planning.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "with deadline",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 16,
"offset": 15
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 16
}
},
"data": {
"hash": 12
}
},
{
"type": "planning",
"keyword": "DEADLINE",
"timestamp": {
"date": "2021-08-19T12:00:00.000Z"
},
"children": [
{
"type": "planning.keyword",
"value": "DEADLINE",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 16
},
"end": {
"line": 2,
"column": 10,
"offset": 25
}
},
"data": {
"hash": 38
}
},
{
"type": "planning.timestamp",
"value": {
"date": "2021-08-19T12:00:00.000Z"
},
"position": {
"start": {
"line": 2,
"column": 10,
"offset": 25
},
"end": {
"line": 2,
"column": 27,
"offset": 42
}
},
"data": {
"hash": 39
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 16
},
"end": {
"line": 2,
"column": 27,
"offset": 42
}
},
"data": {
"hash": 6
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 43
},
"end": {
"line": 3,
"column": 1,
"offset": 43
}
},
"data": {
"hash": 18
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 1,
"offset": 44
}
},
"data": {
"hash": 1
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 44
},
"end": {
"line": 4,
"column": 2,
"offset": 45
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "with deadline and scheduled",
"position": {
"start": {
"line": 4,
"column": 3,
"offset": 46
},
"end": {
"line": 4,
"column": 30,
"offset": 73
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 44
},
"end": {
"line": 5,
"column": 1,
"offset": 74
}
},
"data": {
"hash": 12
}
},
{
"type": "planning",
"keyword": "DEADLINE",
"timestamp": {
"date": "2021-08-26T12:00:00.000Z"
},
"children": [
{
"type": "planning.keyword",
"value": "DEADLINE",
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 74
},
"end": {
"line": 5,
"column": 10,
"offset": 83
}
},
"data": {
"hash": 38
}
},
{
"type": "planning.timestamp",
"value": {
"date": "2021-08-26T12:00:00.000Z"
},
"position": {
"start": {
"line": 5,
"column": 10,
"offset": 83
},
"end": {
"line": 5,
"column": 28,
"offset": 101
}
},
"data": {
"hash": 39
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 74
},
"end": {
"line": 5,
"column": 28,
"offset": 101
}
},
"data": {
"hash": 6
}
},
{
"type": "planning",
"keyword": "SCHEDULED",
"timestamp": {
"date": "2021-08-02T12:00:00.000Z"
},
"children": [
{
"type": "planning.keyword",
"value": "SCHEDULED",
"position": {
"start": {
"line": 5,
"column": 28,
"offset": 101
},
"end": {
"line": 5,
"column": 38,
"offset": 111
}
},
"data": {
"hash": 38
}
},
{
"type": "planning.timestamp",
"value": {
"date": "2021-08-02T12:00:00.000Z"
},
"position": {
"start": {
"line": 5,
"column": 38,
"offset": 111
},
"end": {
"line": 5,
"column": 56,
"offset": 129
}
},
"data": {
"hash": 39
}
}
],
"position": {
"start": {
"line": 5,
"column": 28,
"offset": 101
},
"end": {
"line": 5,
"column": 56,
"offset": 129
}
},
"data": {
"hash": 6
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 44
},
"end": {
"line": 5,
"column": 56,
"offset": 129
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 56,
"offset": 129
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/planning.org
================================================
* with deadline
DEADLINE: <2021-08-20 Fri>
* with deadline and scheduled
DEADLINE: <2021-08-27 Fri> SCHEDULED: <2021-08-03 Tue>
================================================
FILE: packages/orga/src/__tests__/headline/with tags.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline1",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 12,
"offset": 11
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 12
}
},
"data": {
"hash": 12
}
},
{
"type": "section",
"level": 2,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 2,
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 12
},
"end": {
"line": 2,
"column": 3,
"offset": 14
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline2",
"position": {
"start": {
"line": 2,
"column": 4,
"offset": 15
},
"end": {
"line": 2,
"column": 13,
"offset": 24
}
},
"data": {
"hash": 19
}
},
{
"type": "tags",
"tags": [
"tag1",
"tag2"
],
"position": {
"start": {
"line": 2,
"column": 14,
"offset": 25
},
"end": {
"line": 2,
"column": 25,
"offset": 36
}
},
"data": {
"hash": 28
}
}
],
"level": 2,
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 12
},
"end": {
"line": 3,
"column": 1,
"offset": 37
}
},
"tags": [
"tag1",
"tag2"
],
"data": {
"hash": 12
}
},
{
"type": "section",
"level": 3,
"properties": {},
"children": [
{
"type": "headline",
"actionable": false,
"children": [
{
"type": "stars",
"level": 3,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 37
},
"end": {
"line": 3,
"column": 4,
"offset": 40
}
},
"data": {
"hash": 25
}
},
{
"type": "text",
"value": "headline3",
"position": {
"start": {
"line": 3,
"column": 5,
"offset": 41
},
"end": {
"line": 3,
"column": 14,
"offset": 50
}
},
"data": {
"hash": 19
}
},
{
"type": "tags",
"tags": [
"#tag",
"@tag"
],
"position": {
"start": {
"line": 3,
"column": 15,
"offset": 51
},
"end": {
"line": 3,
"column": 26,
"offset": 62
}
},
"data": {
"hash": 28
}
}
],
"level": 3,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 37
},
"end": {
"line": 3,
"column": 27,
"offset": 63
}
},
"tags": [
"#tag",
"@tag"
],
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 37
},
"end": {
"line": 3,
"column": 27,
"offset": 63
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 12
},
"end": {
"line": 3,
"column": 27,
"offset": 63
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 27,
"offset": 63
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 27,
"offset": 63
}
}
}
================================================
FILE: packages/orga/src/__tests__/headline/with tags.org
================================================
* headline1
** headline2 :tag1:tag2:
*** headline3 :#tag:@tag:
================================================
FILE: packages/orga/src/__tests__/hr/standard.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "some text here",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 15,
"offset": 14
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 15,
"offset": 14
},
"end": {
"line": 2,
"column": 1,
"offset": 15
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 15
}
},
"data": {
"hash": 13
}
},
{
"type": "hr",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 15
},
"end": {
"line": 2,
"column": 6,
"offset": 20
}
},
"data": {
"hash": 16
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "some other text here",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 21
},
"end": {
"line": 3,
"column": 21,
"offset": 41
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 21,
"offset": 41
},
"end": {
"line": 4,
"column": 1,
"offset": 42
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "some more",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 42
},
"end": {
"line": 4,
"column": 10,
"offset": 51
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 10,
"offset": 51
},
"end": {
"line": 4,
"column": 11,
"offset": 52
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 21
},
"end": {
"line": 4,
"column": 11,
"offset": 52
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 11,
"offset": 52
}
}
}
================================================
FILE: packages/orga/src/__tests__/hr/standard.org
================================================
some text here
-----
some other text here
some more
================================================
FILE: packages/orga/src/__tests__/index.test.ts
================================================
import assert from 'node:assert'
import { promises as fs, lstatSync, readdirSync } from 'node:fs'
import * as path from 'node:path'
import { describe, it } from 'node:test'
import { parse } from '../index.js'
const specs: { name: string; input: string; output: string }[] = []
// set to true for updating snapshots
const update = false
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const readSpec = (dir: string = __dirname) => {
const files = readdirSync(dir)
for (const file of files) {
const abs = path.join(dir, file)
if (lstatSync(abs).isDirectory()) {
readSpec(abs)
} else {
if (file.endsWith('.org')) {
const name = path
.relative(__dirname, abs)
.split('/')
.join(' ')
.replace(/\.org$/, '')
specs.push({
name,
input: abs,
output: abs.replace(/\.org$/, '.json')
})
}
}
}
}
readSpec()
function removeUndefined(obj: any) {
for (const key in obj) {
if (obj[key] === undefined) {
delete obj[key]
} else if (Array.isArray(obj[key])) {
for (let i = 0; i < obj[key].length; i++) {
removeUndefined(obj[key][i])
}
} else if (typeof obj[key] === 'object') {
removeUndefined(obj[key])
}
}
return obj
}
const dateReviver = (_key: string, value: any) => {
if (typeof value === 'string') {
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/
if (isoDateRegex.test(value)) {
return new Date(value)
}
}
return value
}
describe('parser', () => {
specs.forEach(({ name, input, output }) => {
it(`${name}`, async () => {
const text = await fs.readFile(input, { encoding: 'utf8' })
const tree = parse(text, { timezone: 'Pacific/Auckland' })
if (update) {
await fs.writeFile(output, JSON.stringify(tree, null, 2), 'utf8')
} else {
assert.deepStrictEqual(
removeUndefined(tree),
JSON.parse(
await fs.readFile(output, { encoding: 'utf8' }),
dateReviver
)
)
}
})
})
})
================================================
FILE: packages/orga/src/__tests__/keyword/multiple todo.json
================================================
{
"type": "document",
"properties": {
"todo": [
"TODO NEXT | DONE",
"DRAFT PUBLISHED",
"BUG(b) FEATURE(f) | DONE(d)"
]
},
"children": [
{
"type": "keyword",
"key": "TODO",
"value": "TODO NEXT | DONE",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 36
}
},
{
"type": "keyword",
"key": "TODO",
"value": "DRAFT PUBLISHED",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 25
},
"end": {
"line": 2,
"column": 24,
"offset": 48
}
},
"data": {
"hash": 36
}
},
{
"type": "keyword",
"key": "TODO",
"value": "BUG(b) FEATURE(f) | DONE(d)",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 49
},
"end": {
"line": 3,
"column": 36,
"offset": 84
}
},
"data": {
"hash": 36
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 85
},
"end": {
"line": 4,
"column": 1,
"offset": 85
}
},
"data": {
"hash": 18
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": true,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 86
},
"end": {
"line": 5,
"column": 2,
"offset": 87
}
},
"data": {
"hash": 25
}
},
{
"type": "todo",
"keyword": "DRAFT",
"actionable": true,
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 88
},
"end": {
"line": 5,
"column": 8,
"offset": 93
}
},
"data": {
"hash": 26
}
},
{
"type": "text",
"value": "Some Headline",
"position": {
"start": {
"line": 5,
"column": 9,
"offset": 94
},
"end": {
"line": 5,
"column": 22,
"offset": 107
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 86
},
"end": {
"line": 5,
"column": 23,
"offset": 108
}
},
"keyword": "DRAFT",
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 86
},
"end": {
"line": 5,
"column": 23,
"offset": 108
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 23,
"offset": 108
}
}
}
================================================
FILE: packages/orga/src/__tests__/keyword/multiple todo.org
================================================
#+TODO: TODO NEXT | DONE
#+TODO: DRAFT PUBLISHED
#+TODO: BUG(b) FEATURE(f) | DONE(d)
* DRAFT Some Headline
================================================
FILE: packages/orga/src/__tests__/keyword/other.json
================================================
{
"type": "document",
"properties": {
"title": "this is the title",
"tags": "tag1 tag2"
},
"children": [
{
"type": "keyword",
"key": "TITLE",
"value": "this is the title",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 27,
"offset": 26
}
},
"data": {
"hash": 36
}
},
{
"type": "keyword",
"key": "TAGS",
"value": "tag1 tag2",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 27
},
"end": {
"line": 2,
"column": 18,
"offset": 44
}
},
"data": {
"hash": 36
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 45
},
"end": {
"line": 3,
"column": 1,
"offset": 45
}
},
"data": {
"hash": 18
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "hello",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 46
},
"end": {
"line": 4,
"column": 6,
"offset": 51
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 6,
"offset": 51
},
"end": {
"line": 4,
"column": 7,
"offset": 52
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 46
},
"end": {
"line": 4,
"column": 7,
"offset": 52
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 7,
"offset": 52
}
}
}
================================================
FILE: packages/orga/src/__tests__/keyword/other.org
================================================
#+TITLE: this is the title
#+TAGS: tag1 tag2
hello
================================================
FILE: packages/orga/src/__tests__/keyword/todo.json
================================================
{
"type": "document",
"properties": {
"todo": "TODO NEXT | DONE"
},
"children": [
{
"type": "keyword",
"key": "TODO",
"value": "TODO NEXT | DONE",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 25,
"offset": 24
}
},
"data": {
"hash": 36
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 25
},
"end": {
"line": 2,
"column": 1,
"offset": 25
}
},
"data": {
"hash": 18
}
},
{
"type": "section",
"level": 1,
"properties": {},
"children": [
{
"type": "headline",
"actionable": true,
"children": [
{
"type": "stars",
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 3,
"column": 2,
"offset": 27
}
},
"data": {
"hash": 25
}
},
{
"type": "todo",
"keyword": "NEXT",
"actionable": true,
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 28
},
"end": {
"line": 3,
"column": 7,
"offset": 32
}
},
"data": {
"hash": 26
}
},
{
"type": "text",
"value": "Some Headline",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 33
},
"end": {
"line": 3,
"column": 21,
"offset": 46
}
},
"data": {
"hash": 19
}
}
],
"level": 1,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 3,
"column": 22,
"offset": 47
}
},
"keyword": "NEXT",
"data": {
"hash": 12
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 26
},
"end": {
"line": 3,
"column": 22,
"offset": 47
}
},
"data": {
"hash": 1
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 22,
"offset": 47
}
}
}
================================================
FILE: packages/orga/src/__tests__/keyword/todo.org
================================================
#+TODO: TODO NEXT | DONE
* NEXT Some Headline
================================================
FILE: packages/orga/src/__tests__/latex/does not match.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "\\begin{figure}",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 15,
"offset": 14
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 15,
"offset": 14
},
"end": {
"line": 2,
"column": 1,
"offset": 15
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "x=\\sqrt{b}",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 15
},
"end": {
"line": 2,
"column": 11,
"offset": 25
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 11,
"offset": 25
},
"end": {
"line": 3,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 1,
"offset": 26
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 16,
"offset": 41
}
}
}
================================================
FILE: packages/orga/src/__tests__/latex/does not match.org
================================================
\begin{figure}
x=\sqrt{b}
\end{equation}
================================================
FILE: packages/orga/src/__tests__/latex/latex block.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "latex",
"name": "equation",
"value": "\\begin{equation}\nx=\\sqrt{b}\n\\end{equation}",
"children": [
{
"type": "latex.begin",
"name": "equation",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 17,
"offset": 16
}
},
"data": {
"hash": 33
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 17,
"offset": 16
},
"end": {
"line": 2,
"column": 1,
"offset": 17
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "x=\\sqrt{b}",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 17
},
"end": {
"line": 2,
"column": 11,
"offset": 27
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 11,
"offset": 27
},
"end": {
"line": 3,
"column": 1,
"offset": 28
}
},
"data": {
"hash": 17
}
},
{
"type": "latex.end",
"name": "equation",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 28
},
"end": {
"line": 3,
"column": 16,
"offset": 43
}
},
"data": {
"hash": 34
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 16,
"offset": 43
}
},
"data": {
"hash": 4
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 16,
"offset": 43
}
}
}
================================================
FILE: packages/orga/src/__tests__/latex/latex block.org
================================================
\begin{equation}
x=\sqrt{b}
\end{equation}
================================================
FILE: packages/orga/src/__tests__/latex/missing begin.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "x=\\sqrt{b}",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 11,
"offset": 10
},
"end": {
"line": 2,
"column": 1,
"offset": 11
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 11
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 16,
"offset": 26
}
}
}
================================================
FILE: packages/orga/src/__tests__/latex/missing begin.org
================================================
x=\sqrt{b}
\end{equation}
================================================
FILE: packages/orga/src/__tests__/latex/missing end.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "\\begin{equation}",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 17,
"offset": 16
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 17,
"offset": 16
},
"end": {
"line": 2,
"column": 1,
"offset": 17
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "x=\\sqrt{b}",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 17
},
"end": {
"line": 2,
"column": 11,
"offset": 27
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 11,
"offset": 27
},
"end": {
"line": 2,
"column": 12,
"offset": 28
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 12,
"offset": 28
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 12,
"offset": 28
}
}
}
================================================
FILE: packages/orga/src/__tests__/latex/missing end.org
================================================
\begin{equation}
x=\sqrt{b}
================================================
FILE: packages/orga/src/__tests__/list/broken by empty line.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Apple is an American multinational technology company headquartered in",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 73,
"offset": 72
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 73,
"offset": 72
},
"end": {
"line": 2,
"column": 1,
"offset": 73
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": " Cupertino, California that designs, develops, and sells consumer electronics,",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 73
},
"end": {
"line": 2,
"column": 80,
"offset": 152
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 80,
"offset": 152
},
"end": {
"line": 3,
"column": 1,
"offset": 153
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 153
},
"end": {
"line": 3,
"column": 1,
"offset": 153
}
},
"data": {
"hash": 18
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 1,
"offset": 153
}
},
"data": {
"hash": 11
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 153
},
"end": {
"line": 4,
"column": 1,
"offset": 154
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 1,
"offset": 154
}
},
"data": {
"hash": 7
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": " computer software, and online services.",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 154
},
"end": {
"line": 4,
"column": 42,
"offset": 195
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 42,
"offset": 195
},
"end": {
"line": 5,
"column": 1,
"offset": 196
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 154
},
"end": {
"line": 5,
"column": 1,
"offset": 196
}
},
"data": {
"hash": 13
}
},
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 196
},
"end": {
"line": 5,
"column": 2,
"offset": 197
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Orange",
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 198
},
"end": {
"line": 5,
"column": 9,
"offset": 204
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 5,
"column": 9,
"offset": 204
},
"end": {
"line": 6,
"column": 1,
"offset": 205
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 196
},
"end": {
"line": 6,
"column": 1,
"offset": 205
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 205
},
"end": {
"line": 6,
"column": 2,
"offset": 206
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Banana",
"position": {
"start": {
"line": 6,
"column": 3,
"offset": 207
},
"end": {
"line": 6,
"column": 9,
"offset": 213
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 6,
"column": 9,
"offset": 213
},
"end": {
"line": 6,
"column": 10,
"offset": 214
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 205
},
"end": {
"line": 6,
"column": 10,
"offset": 214
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 196
},
"end": {
"line": 6,
"column": 10,
"offset": 214
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 10,
"offset": 214
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/broken by empty line.org
================================================
- Apple is an American multinational technology company headquartered in
Cupertino, California that designs, develops, and sells consumer electronics,
computer software, and online services.
- Orange
- Banana
================================================
FILE: packages/orga/src/__tests__/list/descriptive.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 42
}
},
{
"type": "list.item.tag",
"value": "Apple",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 8,
"offset": 7
}
},
"data": {
"hash": 40
}
},
{
"type": "text",
"value": "it's apple",
"position": {
"start": {
"line": 1,
"column": 12,
"offset": 11
},
"end": {
"line": 1,
"column": 22,
"offset": 21
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 22,
"offset": 21
},
"end": {
"line": 2,
"column": 1,
"offset": 22
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 22
}
},
"tag": "Apple",
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 22
},
"end": {
"line": 2,
"column": 2,
"offset": 23
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Orange",
"position": {
"start": {
"line": 2,
"column": 3,
"offset": 24
},
"end": {
"line": 2,
"column": 9,
"offset": 30
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 9,
"offset": 30
},
"end": {
"line": 3,
"column": 1,
"offset": 31
}
},
"data": {
"hash": 17
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 31
},
"end": {
"line": 3,
"column": 1,
"offset": 31
}
},
"data": {
"hash": 18
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 22
},
"end": {
"line": 3,
"column": 1,
"offset": 31
}
},
"data": {
"hash": 11
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 31
},
"end": {
"line": 4,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 7
}
},
{
"type": "emptyLine",
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 32
},
"end": {
"line": 4,
"column": 1,
"offset": 32
}
},
"data": {
"hash": 18
}
},
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 33
},
"end": {
"line": 5,
"column": 2,
"offset": 34
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "XBox",
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 35
},
"end": {
"line": 5,
"column": 7,
"offset": 39
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 5,
"column": 7,
"offset": 39
},
"end": {
"line": 6,
"column": 1,
"offset": 40
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 33
},
"end": {
"line": 6,
"column": 1,
"offset": 40
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 40
},
"end": {
"line": 6,
"column": 2,
"offset": 41
}
},
"data": {
"hash": 42
}
},
{
"type": "list.item.tag",
"value": "Play Station",
"position": {
"start": {
"line": 6,
"column": 3,
"offset": 42
},
"end": {
"line": 6,
"column": 15,
"offset": 54
}
},
"data": {
"hash": 40
}
},
{
"type": "text",
"value": "for the gamers",
"position": {
"start": {
"line": 6,
"column": 19,
"offset": 58
},
"end": {
"line": 6,
"column": 33,
"offset": 72
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 6,
"column": 33,
"offset": 72
},
"end": {
"line": 6,
"column": 34,
"offset": 73
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 6,
"column": 1,
"offset": 40
},
"end": {
"line": 6,
"column": 34,
"offset": 73
}
},
"tag": "Play Station",
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 33
},
"end": {
"line": 6,
"column": 34,
"offset": 73
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 34,
"offset": 73
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/descriptive.org
================================================
- Apple :: it's apple
- Orange
- XBox
- Play Station :: for the gamers
================================================
FILE: packages/orga/src/__tests__/list/multiline.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Apple is an American multinational technology company headquartered in",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 73,
"offset": 72
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 73,
"offset": 72
},
"end": {
"line": 2,
"column": 1,
"offset": 73
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "Cupertino, California that designs, develops, and sells consumer electronics,",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 73
},
"end": {
"line": 2,
"column": 78,
"offset": 150
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 78,
"offset": 150
},
"end": {
"line": 3,
"column": 1,
"offset": 151
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "computer software, and online services.",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 151
},
"end": {
"line": 3,
"column": 40,
"offset": 190
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 40,
"offset": 190
},
"end": {
"line": 4,
"column": 1,
"offset": 191
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 1,
"offset": 191
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 191
},
"end": {
"line": 4,
"column": 2,
"offset": 192
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Orange",
"position": {
"start": {
"line": 4,
"column": 3,
"offset": 193
},
"end": {
"line": 4,
"column": 9,
"offset": 199
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 9,
"offset": 199
},
"end": {
"line": 5,
"column": 1,
"offset": 200
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 191
},
"end": {
"line": 5,
"column": 1,
"offset": 200
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 200
},
"end": {
"line": 5,
"column": 2,
"offset": 201
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Banana",
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 202
},
"end": {
"line": 5,
"column": 9,
"offset": 208
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 5,
"column": 9,
"offset": 208
},
"end": {
"line": 5,
"column": 10,
"offset": 209
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 200
},
"end": {
"line": 5,
"column": 10,
"offset": 209
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 10,
"offset": 209
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 10,
"offset": 209
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/multiline.org
================================================
- Apple is an American multinational technology company headquartered in
Cupertino, California that designs, develops, and sells consumer electronics,
computer software, and online services.
- Orange
- Banana
================================================
FILE: packages/orga/src/__tests__/list/nested.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": true,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": true,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 3,
"offset": 2
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "apple",
"position": {
"start": {
"line": 1,
"column": 4,
"offset": 3
},
"end": {
"line": 1,
"column": 9,
"offset": 8
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 9,
"offset": 8
},
"end": {
"line": 2,
"column": 1,
"offset": 9
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 3,
"offset": 11
}
},
"data": {
"hash": 11
}
},
{
"type": "list",
"indent": 2,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 2,
"children": [
{
"type": "list.item.bullet",
"indent": 2,
"ordered": false,
"position": {
"start": {
"line": 2,
"column": 3,
"offset": 11
},
"end": {
"line": 2,
"column": 4,
"offset": 12
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "iPhone",
"position": {
"start": {
"line": 2,
"column": 5,
"offset": 13
},
"end": {
"line": 2,
"column": 11,
"offset": 19
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 11,
"offset": 19
},
"end": {
"line": 3,
"column": 1,
"offset": 20
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 2,
"column": 3,
"offset": 11
},
"end": {
"line": 3,
"column": 3,
"offset": 22
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 2,
"children": [
{
"type": "list.item.bullet",
"indent": 2,
"ordered": false,
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 22
},
"end": {
"line": 3,
"column": 4,
"offset": 23
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "Mac",
"position": {
"start": {
"line": 3,
"column": 5,
"offset": 24
},
"end": {
"line": 3,
"column": 8,
"offset": 27
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 8,
"offset": 27
},
"end": {
"line": 4,
"column": 1,
"offset": 28
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 22
},
"end": {
"line": 4,
"column": 1,
"offset": 28
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 2,
"column": 3,
"offset": 11
},
"end": {
"line": 4,
"column": 1,
"offset": 28
}
},
"data": {
"hash": 7
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": true,
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 28
},
"end": {
"line": 4,
"column": 3,
"offset": 30
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "banana",
"position": {
"start": {
"line": 4,
"column": 4,
"offset": 31
},
"end": {
"line": 4,
"column": 10,
"offset": 37
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 4,
"column": 10,
"offset": 37
},
"end": {
"line": 5,
"column": 1,
"offset": 38
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 28
},
"end": {
"line": 5,
"column": 1,
"offset": 38
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 38
},
"end": {
"line": 5,
"column": 2,
"offset": 39
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "orange",
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 40
},
"end": {
"line": 5,
"column": 9,
"offset": 46
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 5,
"column": 9,
"offset": 46
},
"end": {
"line": 5,
"column": 10,
"offset": 47
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 38
},
"end": {
"line": 5,
"column": 10,
"offset": 47
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 10,
"offset": 47
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 10,
"offset": 47
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/nested.org
================================================
1. apple
- iPhone
- Mac
5. banana
- orange
================================================
FILE: packages/orga/src/__tests__/list/ordered.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": true,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": true,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 3,
"offset": 2
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "apple",
"position": {
"start": {
"line": 1,
"column": 4,
"offset": 3
},
"end": {
"line": 1,
"column": 9,
"offset": 8
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 9,
"offset": 8
},
"end": {
"line": 2,
"column": 1,
"offset": 9
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 9
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": true,
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 9
},
"end": {
"line": 2,
"column": 3,
"offset": 11
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "banana",
"position": {
"start": {
"line": 2,
"column": 4,
"offset": 12
},
"end": {
"line": 2,
"column": 10,
"offset": 18
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 10,
"offset": 18
},
"end": {
"line": 3,
"column": 1,
"offset": 19
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 9
},
"end": {
"line": 3,
"column": 1,
"offset": 19
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 19
},
"end": {
"line": 3,
"column": 2,
"offset": 20
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "orange",
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 21
},
"end": {
"line": 3,
"column": 9,
"offset": 27
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 9,
"offset": 27
},
"end": {
"line": 3,
"column": 10,
"offset": 28
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 19
},
"end": {
"line": 3,
"column": 10,
"offset": 28
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 10,
"offset": 28
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 10,
"offset": 28
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/ordered.org
================================================
1. apple
5. banana
- orange
================================================
FILE: packages/orga/src/__tests__/list/unordered.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "list",
"indent": 0,
"ordered": false,
"children": [
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 2,
"offset": 1
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "apple",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 8,
"offset": 7
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 2,
"column": 1,
"offset": 8
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 8
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 8
},
"end": {
"line": 2,
"column": 2,
"offset": 9
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "banana",
"position": {
"start": {
"line": 2,
"column": 3,
"offset": 10
},
"end": {
"line": 2,
"column": 9,
"offset": 16
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 9,
"offset": 16
},
"end": {
"line": 3,
"column": 1,
"offset": 17
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 8
},
"end": {
"line": 3,
"column": 1,
"offset": 17
}
},
"data": {
"hash": 11
}
},
{
"type": "list.item",
"indent": 0,
"children": [
{
"type": "list.item.bullet",
"indent": 0,
"ordered": false,
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 17
},
"end": {
"line": 3,
"column": 2,
"offset": 18
}
},
"data": {
"hash": 42
}
},
{
"type": "text",
"value": "orange",
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 19
},
"end": {
"line": 3,
"column": 9,
"offset": 25
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 3,
"column": 9,
"offset": 25
},
"end": {
"line": 3,
"column": 10,
"offset": 26
}
},
"data": {
"hash": 17
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 17
},
"end": {
"line": 3,
"column": 10,
"offset": 26
}
},
"data": {
"hash": 11
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 10,
"offset": 26
}
},
"data": {
"hash": 7
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 10,
"offset": 26
}
}
}
================================================
FILE: packages/orga/src/__tests__/list/unordered.org
================================================
- apple
- banana
- orange
================================================
FILE: packages/orga/src/__tests__/paragraph/anonymous footnote.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 10,
"offset": 9
}
},
"data": {
"hash": 22
}
},
{
"type": "text",
"value": "anonymous footnote",
"position": {
"start": {
"line": 1,
"column": 11,
"offset": 10
},
"end": {
"line": 1,
"column": 29,
"offset": 28
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 29,
"offset": 28
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 24
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 30,
"offset": 29
},
"end": {
"line": 1,
"column": 31,
"offset": 30
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 31,
"offset": 30
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/anonymous footnote.org
================================================
with [fn::anonymous footnote].
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote reference at the end.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with footnote at the end.",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "2",
"position": {
"start": {
"line": 1,
"column": 30,
"offset": 29
},
"end": {
"line": 1,
"column": 31,
"offset": 30
}
},
"data": {
"hash": 37
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 31,
"offset": 30
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"label": "2",
"data": {
"hash": 24
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote reference at the end.org
================================================
with footnote at the end.[fn:2]
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote refernece in the middle.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with footnote",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 14,
"offset": 13
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 14,
"offset": 13
},
"end": {
"line": 1,
"column": 18,
"offset": 17
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 18,
"offset": 17
},
"end": {
"line": 1,
"column": 19,
"offset": 18
}
},
"data": {
"hash": 37
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 19,
"offset": 18
},
"end": {
"line": 1,
"column": 20,
"offset": 19
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 14,
"offset": 13
},
"end": {
"line": 1,
"column": 20,
"offset": 19
}
},
"label": "1",
"data": {
"hash": 24
}
},
{
"type": "text",
"value": " in the middle.",
"position": {
"start": {
"line": 1,
"column": 20,
"offset": 19
},
"end": {
"line": 1,
"column": 35,
"offset": 34
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 35,
"offset": 34
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote refernece in the middle.org
================================================
with footnote[fn:1] in the middle.
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote without body.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with anonymous footnote without body",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 37,
"offset": 36
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 37,
"offset": 36
},
"end": {
"line": 1,
"column": 41,
"offset": 40
}
},
"data": {
"hash": 22
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 42,
"offset": 41
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 37,
"offset": 36
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
},
"data": {
"hash": 24
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 43,
"offset": 42
},
"end": {
"line": 1,
"column": 44,
"offset": 43
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 44,
"offset": 43
},
"end": {
"line": 1,
"column": 45,
"offset": 44
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 45,
"offset": 44
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 45,
"offset": 44
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/footnote without body.org
================================================
with anonymous footnote without body[fn::].
================================================
FILE: packages/orga/src/__tests__/paragraph/inline footnote with style.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 10,
"offset": 9
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "3",
"position": {
"start": {
"line": 1,
"column": 10,
"offset": 9
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
},
"data": {
"hash": 37
}
},
{
"type": "text",
"value": "inline footnote ",
"position": {
"start": {
"line": 1,
"column": 12,
"offset": 11
},
"end": {
"line": 1,
"column": 28,
"offset": 27
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "italic",
"value": "with",
"position": {
"start": {
"line": 1,
"column": 28,
"offset": 27
},
"end": {
"line": 1,
"column": 34,
"offset": 33
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " style",
"position": {
"start": {
"line": 1,
"column": 34,
"offset": 33
},
"end": {
"line": 1,
"column": 40,
"offset": 39
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 40,
"offset": 39
},
"end": {
"line": 1,
"column": 41,
"offset": 40
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 41,
"offset": 40
}
},
"label": "3",
"data": {
"hash": 24
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 41,
"offset": 40
},
"end": {
"line": 1,
"column": 42,
"offset": 41
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 42,
"offset": 41
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/inline footnote with style.org
================================================
with [fn:3:inline footnote /with/ style].
================================================
FILE: packages/orga/src/__tests__/paragraph/inline footnote.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 10,
"offset": 9
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "3",
"position": {
"start": {
"line": 1,
"column": 10,
"offset": 9
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
},
"data": {
"hash": 37
}
},
{
"type": "text",
"value": "inline footnote",
"position": {
"start": {
"line": 1,
"column": 12,
"offset": 11
},
"end": {
"line": 1,
"column": 27,
"offset": 26
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 27,
"offset": 26
},
"end": {
"line": 1,
"column": 28,
"offset": 27
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 28,
"offset": 27
}
},
"label": "3",
"data": {
"hash": 24
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 28,
"offset": 27
},
"end": {
"line": 1,
"column": 29,
"offset": 28
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 29,
"offset": 28
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/inline footnote.org
================================================
with [fn:3:inline footnote].
================================================
FILE: packages/orga/src/__tests__/paragraph/inline math.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "If ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 4,
"offset": 3
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "math",
"value": "a^2=b",
"position": {
"start": {
"line": 1,
"column": 4,
"offset": 3
},
"end": {
"line": 1,
"column": 13,
"offset": 12
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " and ",
"position": {
"start": {
"line": 1,
"column": 13,
"offset": 12
},
"end": {
"line": 1,
"column": 18,
"offset": 17
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "math",
"value": " b=2 ",
"position": {
"start": {
"line": 1,
"column": 18,
"offset": 17
},
"end": {
"line": 1,
"column": 27,
"offset": 26
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", then the solution must be",
"position": {
"start": {
"line": 1,
"column": 27,
"offset": 26
},
"end": {
"line": 1,
"column": 54,
"offset": 53
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 54,
"offset": 53
},
"end": {
"line": 2,
"column": 1,
"offset": 54
}
},
"data": {
"hash": 17
}
},
{
"type": "text",
"value": "either ",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 54
},
"end": {
"line": 2,
"column": 8,
"offset": 61
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "math",
"value": " a=+\\sqrt{2} ",
"position": {
"start": {
"line": 2,
"column": 8,
"offset": 61
},
"end": {
"line": 2,
"column": 25,
"offset": 78
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " or ",
"position": {
"start": {
"line": 2,
"column": 25,
"offset": 78
},
"end": {
"line": 2,
"column": 29,
"offset": 82
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "math",
"value": " a=-\\sqrt{2} ",
"position": {
"start": {
"line": 2,
"column": 29,
"offset": 82
},
"end": {
"line": 2,
"column": 46,
"offset": 99
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 2,
"column": 46,
"offset": 99
},
"end": {
"line": 2,
"column": 47,
"offset": 100
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 2,
"column": 47,
"offset": 100
},
"end": {
"line": 2,
"column": 48,
"offset": 101
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 48,
"offset": 101
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 48,
"offset": 101
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/inline math.org
================================================
If $$a^2=b$$ and \( b=2 \), then the solution must be
either $$ a=+\sqrt{2} $$ or \[ a=-\sqrt{2} \].
================================================
FILE: packages/orga/src/__tests__/paragraph/link with style.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "test ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "link",
"children": [
{
"type": "opening",
"element": "link",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 7,
"offset": 6
}
},
"data": {
"hash": 22
}
},
{
"type": "link.path",
"protocol": "https",
"value": "https://orga.js.org/",
"position": {
"start": {
"line": 1,
"column": 7,
"offset": 6
},
"end": {
"line": 1,
"column": 29,
"offset": 28
}
},
"data": {
"hash": 21
}
},
{
"type": "text",
"style": "bold",
"value": "link",
"position": {
"start": {
"line": 1,
"column": 30,
"offset": 29
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " with ",
"position": {
"start": {
"line": 1,
"column": 36,
"offset": 35
},
"end": {
"line": 1,
"column": 42,
"offset": 41
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "italic",
"value": "style",
"position": {
"start": {
"line": 1,
"column": 42,
"offset": 41
},
"end": {
"line": 1,
"column": 49,
"offset": 48
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "link",
"position": {
"start": {
"line": 1,
"column": 50,
"offset": 49
},
"end": {
"line": 1,
"column": 51,
"offset": 50
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 51,
"offset": 50
}
},
"path": {
"protocol": "https",
"value": "https://orga.js.org/"
},
"attributes": {},
"data": {
"hash": 20
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 51,
"offset": 50
},
"end": {
"line": 1,
"column": 52,
"offset": 51
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 52,
"offset": 51
},
"end": {
"line": 1,
"column": 53,
"offset": 52
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 53,
"offset": 52
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 53,
"offset": 52
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/link with style.org
================================================
test [[https://orga.js.org/][*link* with /style/]].
================================================
FILE: packages/orga/src/__tests__/paragraph/link.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "it's a ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 8,
"offset": 7
}
},
"data": {
"hash": 19
}
},
{
"type": "link",
"children": [
{
"type": "opening",
"element": "link",
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 1,
"column": 9,
"offset": 8
}
},
"data": {
"hash": 22
}
},
{
"type": "link.path",
"protocol": "https",
"value": "https://orga.js.org/",
"position": {
"start": {
"line": 1,
"column": 9,
"offset": 8
},
"end": {
"line": 1,
"column": 31,
"offset": 30
}
},
"data": {
"hash": 21
}
},
{
"type": "text",
"value": "link",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "link",
"position": {
"start": {
"line": 1,
"column": 37,
"offset": 36
},
"end": {
"line": 1,
"column": 38,
"offset": 37
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 8,
"offset": 7
},
"end": {
"line": 1,
"column": 38,
"offset": 37
}
},
"path": {
"protocol": "https",
"value": "https://orga.js.org/"
},
"attributes": {},
"data": {
"hash": 20
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 38,
"offset": 37
},
"end": {
"line": 1,
"column": 39,
"offset": 38
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 39,
"offset": 38
},
"end": {
"line": 1,
"column": 40,
"offset": 39
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 40,
"offset": 39
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 40,
"offset": 39
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/link.org
================================================
it's a [[https://orga.js.org/][link]].
================================================
FILE: packages/orga/src/__tests__/paragraph/nested footnote.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "with ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 10,
"offset": 9
}
},
"data": {
"hash": 22
}
},
{
"type": "text",
"value": "nested ",
"position": {
"start": {
"line": 1,
"column": 11,
"offset": 10
},
"end": {
"line": 1,
"column": 18,
"offset": 17
}
},
"data": {
"hash": 19
}
},
{
"type": "footnote.reference",
"children": [
{
"type": "opening",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 18,
"offset": 17
},
"end": {
"line": 1,
"column": 22,
"offset": 21
}
},
"data": {
"hash": 22
}
},
{
"type": "footnote.label",
"label": "1",
"position": {
"start": {
"line": 1,
"column": 22,
"offset": 21
},
"end": {
"line": 1,
"column": 23,
"offset": 22
}
},
"data": {
"hash": 37
}
},
{
"type": "text",
"value": "footnote",
"position": {
"start": {
"line": 1,
"column": 24,
"offset": 23
},
"end": {
"line": 1,
"column": 32,
"offset": 31
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 32,
"offset": 31
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 18,
"offset": 17
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"label": "1",
"data": {
"hash": 24
}
},
{
"type": "closing",
"element": "footnote.reference",
"position": {
"start": {
"line": 1,
"column": 33,
"offset": 32
},
"end": {
"line": 1,
"column": 34,
"offset": 33
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 34,
"offset": 33
}
},
"data": {
"hash": 24
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 34,
"offset": 33
},
"end": {
"line": 1,
"column": 35,
"offset": 34
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 35,
"offset": 34
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 35,
"offset": 34
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/nested footnote.org
================================================
with [fn::nested [fn:1:footnote]]
================================================
FILE: packages/orga/src/__tests__/paragraph/styled text.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "text ",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "bold",
"value": "bold",
"position": {
"start": {
"line": 1,
"column": 6,
"offset": 5
},
"end": {
"line": 1,
"column": 12,
"offset": 11
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", ",
"position": {
"start": {
"line": 1,
"column": 12,
"offset": 11
},
"end": {
"line": 1,
"column": 14,
"offset": 13
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "verbatim",
"value": "verbatim",
"position": {
"start": {
"line": 1,
"column": 14,
"offset": 13
},
"end": {
"line": 1,
"column": 24,
"offset": 23
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", ",
"position": {
"start": {
"line": 1,
"column": 24,
"offset": 23
},
"end": {
"line": 1,
"column": 26,
"offset": 25
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "italic",
"value": "italic",
"position": {
"start": {
"line": 1,
"column": 26,
"offset": 25
},
"end": {
"line": 1,
"column": 34,
"offset": 33
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", ",
"position": {
"start": {
"line": 1,
"column": 34,
"offset": 33
},
"end": {
"line": 1,
"column": 36,
"offset": 35
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "strikeThrough",
"value": "strike through",
"position": {
"start": {
"line": 1,
"column": 36,
"offset": 35
},
"end": {
"line": 1,
"column": 52,
"offset": 51
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", ",
"position": {
"start": {
"line": 1,
"column": 52,
"offset": 51
},
"end": {
"line": 1,
"column": 54,
"offset": 53
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "underline",
"value": "underline",
"position": {
"start": {
"line": 1,
"column": 54,
"offset": 53
},
"end": {
"line": 1,
"column": 65,
"offset": 64
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ", ",
"position": {
"start": {
"line": 1,
"column": 65,
"offset": 64
},
"end": {
"line": 1,
"column": 67,
"offset": 66
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "code",
"value": "code",
"position": {
"start": {
"line": 1,
"column": 67,
"offset": 66
},
"end": {
"line": 1,
"column": 73,
"offset": 72
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 1,
"column": 73,
"offset": 72
},
"end": {
"line": 1,
"column": 74,
"offset": 73
}
},
"data": {
"hash": 19
}
},
{
"type": "newline",
"position": {
"start": {
"line": 1,
"column": 74,
"offset": 73
},
"end": {
"line": 1,
"column": 75,
"offset": 74
}
},
"data": {
"hash": 17
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 75,
"offset": 74
}
},
"data": {
"hash": 13
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 75,
"offset": 74
}
}
}
================================================
FILE: packages/orga/src/__tests__/paragraph/styled text.org
================================================
text *bold*, =verbatim=, /italic/, +strike through+, _underline_, ~code~.
================================================
FILE: packages/orga/src/__tests__/table/standard.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "table",
"children": [
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Name ",
"position": {
"start": {
"line": 1,
"column": 2,
"offset": 1
},
"end": {
"line": 1,
"column": 16,
"offset": 15
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 2,
"offset": 1
},
"end": {
"line": 1,
"column": 16,
"offset": 15
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 16,
"offset": 15
},
"end": {
"line": 1,
"column": 17,
"offset": 16
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Species ",
"position": {
"start": {
"line": 1,
"column": 17,
"offset": 16
},
"end": {
"line": 1,
"column": 29,
"offset": 28
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 17,
"offset": 16
},
"end": {
"line": 1,
"column": 29,
"offset": 28
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 29,
"offset": 28
},
"end": {
"line": 1,
"column": 30,
"offset": 29
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Gender ",
"position": {
"start": {
"line": 1,
"column": 30,
"offset": 29
},
"end": {
"line": 1,
"column": 38,
"offset": 37
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 30,
"offset": 29
},
"end": {
"line": 1,
"column": 38,
"offset": 37
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 38,
"offset": 37
},
"end": {
"line": 1,
"column": 39,
"offset": 38
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Role ",
"position": {
"start": {
"line": 1,
"column": 39,
"offset": 38
},
"end": {
"line": 1,
"column": 53,
"offset": 52
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 39,
"offset": 38
},
"end": {
"line": 1,
"column": 53,
"offset": 52
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 53,
"offset": 52
},
"end": {
"line": 1,
"column": 54,
"offset": 53
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 54
}
},
"data": {
"hash": 9
}
},
{
"type": "table.hr",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 54
},
"end": {
"line": 2,
"column": 54,
"offset": 107
}
},
"data": {
"hash": 43
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Bruce Wayne ",
"position": {
"start": {
"line": 3,
"column": 2,
"offset": 109
},
"end": {
"line": 3,
"column": 16,
"offset": 123
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 2,
"offset": 109
},
"end": {
"line": 3,
"column": 16,
"offset": 123
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 16,
"offset": 123
},
"end": {
"line": 3,
"column": 17,
"offset": 124
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Human ",
"position": {
"start": {
"line": 3,
"column": 17,
"offset": 124
},
"end": {
"line": 3,
"column": 29,
"offset": 136
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 17,
"offset": 124
},
"end": {
"line": 3,
"column": 29,
"offset": 136
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 29,
"offset": 136
},
"end": {
"line": 3,
"column": 30,
"offset": 137
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " M ",
"position": {
"start": {
"line": 3,
"column": 30,
"offset": 137
},
"end": {
"line": 3,
"column": 38,
"offset": 145
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 30,
"offset": 137
},
"end": {
"line": 3,
"column": 38,
"offset": 145
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 38,
"offset": 145
},
"end": {
"line": 3,
"column": 39,
"offset": 146
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Batman ",
"position": {
"start": {
"line": 3,
"column": 39,
"offset": 146
},
"end": {
"line": 3,
"column": 53,
"offset": 160
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 39,
"offset": 146
},
"end": {
"line": 3,
"column": 53,
"offset": 160
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 53,
"offset": 160
},
"end": {
"line": 3,
"column": 54,
"offset": 161
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 108
},
"end": {
"line": 4,
"column": 1,
"offset": 162
}
},
"data": {
"hash": 9
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Clark Kent ",
"position": {
"start": {
"line": 4,
"column": 2,
"offset": 163
},
"end": {
"line": 4,
"column": 16,
"offset": 177
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 2,
"offset": 163
},
"end": {
"line": 4,
"column": 16,
"offset": 177
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 16,
"offset": 177
},
"end": {
"line": 4,
"column": 17,
"offset": 178
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Kryptonian ",
"position": {
"start": {
"line": 4,
"column": 17,
"offset": 178
},
"end": {
"line": 4,
"column": 29,
"offset": 190
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 17,
"offset": 178
},
"end": {
"line": 4,
"column": 29,
"offset": 190
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 29,
"offset": 190
},
"end": {
"line": 4,
"column": 30,
"offset": 191
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " M ",
"position": {
"start": {
"line": 4,
"column": 30,
"offset": 191
},
"end": {
"line": 4,
"column": 38,
"offset": 199
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 30,
"offset": 191
},
"end": {
"line": 4,
"column": 38,
"offset": 199
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 38,
"offset": 199
},
"end": {
"line": 4,
"column": 39,
"offset": 200
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Superman ",
"position": {
"start": {
"line": 4,
"column": 39,
"offset": 200
},
"end": {
"line": 4,
"column": 53,
"offset": 214
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 39,
"offset": 200
},
"end": {
"line": 4,
"column": 53,
"offset": 214
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 53,
"offset": 214
},
"end": {
"line": 4,
"column": 54,
"offset": 215
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 162
},
"end": {
"line": 5,
"column": 1,
"offset": 216
}
},
"data": {
"hash": 9
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Diana Prince ",
"position": {
"start": {
"line": 5,
"column": 2,
"offset": 217
},
"end": {
"line": 5,
"column": 16,
"offset": 231
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 2,
"offset": 217
},
"end": {
"line": 5,
"column": 16,
"offset": 231
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 16,
"offset": 231
},
"end": {
"line": 5,
"column": 17,
"offset": 232
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Amazonian ",
"position": {
"start": {
"line": 5,
"column": 17,
"offset": 232
},
"end": {
"line": 5,
"column": 29,
"offset": 244
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 17,
"offset": 232
},
"end": {
"line": 5,
"column": 29,
"offset": 244
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 29,
"offset": 244
},
"end": {
"line": 5,
"column": 30,
"offset": 245
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " F ",
"position": {
"start": {
"line": 5,
"column": 30,
"offset": 245
},
"end": {
"line": 5,
"column": 38,
"offset": 253
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 30,
"offset": 245
},
"end": {
"line": 5,
"column": 38,
"offset": 253
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 38,
"offset": 253
},
"end": {
"line": 5,
"column": 39,
"offset": 254
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Wonder Woman ",
"position": {
"start": {
"line": 5,
"column": 39,
"offset": 254
},
"end": {
"line": 5,
"column": 53,
"offset": 268
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 39,
"offset": 254
},
"end": {
"line": 5,
"column": 53,
"offset": 268
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 53,
"offset": 268
},
"end": {
"line": 5,
"column": 54,
"offset": 269
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 216
},
"end": {
"line": 5,
"column": 55,
"offset": 270
}
},
"data": {
"hash": 9
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 55,
"offset": 270
}
},
"data": {
"hash": 8
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 55,
"offset": 270
}
}
}
================================================
FILE: packages/orga/src/__tests__/table/standard.org
================================================
| Name | Species | Gender | Role |
|--------------+------------+--------+--------------|
| Bruce Wayne | Human | M | Batman |
| Clark Kent | Kryptonian | M | Superman |
| Diana Prince | Amazonian | F | Wonder Woman |
================================================
FILE: packages/orga/src/__tests__/table/with inline styles.json
================================================
{
"type": "document",
"properties": {},
"children": [
{
"type": "table",
"children": [
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Name ",
"position": {
"start": {
"line": 1,
"column": 2,
"offset": 1
},
"end": {
"line": 1,
"column": 18,
"offset": 17
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 2,
"offset": 1
},
"end": {
"line": 1,
"column": 18,
"offset": 17
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 18,
"offset": 17
},
"end": {
"line": 1,
"column": 19,
"offset": 18
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Species ",
"position": {
"start": {
"line": 1,
"column": 19,
"offset": 18
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 19,
"offset": 18
},
"end": {
"line": 1,
"column": 33,
"offset": 32
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 33,
"offset": 32
},
"end": {
"line": 1,
"column": 34,
"offset": 33
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Gender ",
"position": {
"start": {
"line": 1,
"column": 34,
"offset": 33
},
"end": {
"line": 1,
"column": 42,
"offset": 41
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 34,
"offset": 33
},
"end": {
"line": 1,
"column": 42,
"offset": 41
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 42,
"offset": 41
},
"end": {
"line": 1,
"column": 43,
"offset": 42
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " Role ",
"position": {
"start": {
"line": 1,
"column": 43,
"offset": 42
},
"end": {
"line": 1,
"column": 57,
"offset": 56
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 1,
"column": 43,
"offset": 42
},
"end": {
"line": 1,
"column": 57,
"offset": 56
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 1,
"column": 57,
"offset": 56
},
"end": {
"line": 1,
"column": 58,
"offset": 57
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 2,
"column": 1,
"offset": 58
}
},
"data": {
"hash": 9
}
},
{
"type": "table.hr",
"position": {
"start": {
"line": 2,
"column": 1,
"offset": 58
},
"end": {
"line": 2,
"column": 58,
"offset": 115
}
},
"data": {
"hash": 43
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 3,
"column": 2,
"offset": 117
},
"end": {
"line": 3,
"column": 3,
"offset": 118
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "bold",
"value": "Bruce Wayne",
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 118
},
"end": {
"line": 3,
"column": 16,
"offset": 131
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 3,
"column": 16,
"offset": 131
},
"end": {
"line": 3,
"column": 18,
"offset": 133
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 2,
"offset": 117
},
"end": {
"line": 3,
"column": 18,
"offset": 133
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 18,
"offset": 133
},
"end": {
"line": 3,
"column": 19,
"offset": 134
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 3,
"column": 19,
"offset": 134
},
"end": {
"line": 3,
"column": 20,
"offset": 135
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "strikeThrough",
"value": "Bat",
"position": {
"start": {
"line": 3,
"column": 20,
"offset": 135
},
"end": {
"line": 3,
"column": 25,
"offset": 140
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " Human ",
"position": {
"start": {
"line": 3,
"column": 25,
"offset": 140
},
"end": {
"line": 3,
"column": 33,
"offset": 148
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 19,
"offset": 134
},
"end": {
"line": 3,
"column": 33,
"offset": 148
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 33,
"offset": 148
},
"end": {
"line": 3,
"column": 34,
"offset": 149
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " M ",
"position": {
"start": {
"line": 3,
"column": 34,
"offset": 149
},
"end": {
"line": 3,
"column": 42,
"offset": 157
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 34,
"offset": 149
},
"end": {
"line": 3,
"column": 42,
"offset": 157
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 42,
"offset": 157
},
"end": {
"line": 3,
"column": 43,
"offset": 158
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 3,
"column": 43,
"offset": 158
},
"end": {
"line": 3,
"column": 44,
"offset": 159
}
},
"data": {
"hash": 19
}
},
{
"type": "link",
"children": [
{
"type": "opening",
"element": "link",
"position": {
"start": {
"line": 3,
"column": 44,
"offset": 159
},
"end": {
"line": 3,
"column": 45,
"offset": 160
}
},
"data": {
"hash": 22
}
},
{
"type": "link.path",
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Batman",
"position": {
"start": {
"line": 3,
"column": 45,
"offset": 160
},
"end": {
"line": 3,
"column": 83,
"offset": 198
}
},
"data": {
"hash": 21
}
},
{
"type": "text",
"value": "Batman",
"position": {
"start": {
"line": 3,
"column": 84,
"offset": 199
},
"end": {
"line": 3,
"column": 90,
"offset": 205
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "link",
"position": {
"start": {
"line": 3,
"column": 91,
"offset": 206
},
"end": {
"line": 3,
"column": 92,
"offset": 207
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 3,
"column": 44,
"offset": 159
},
"end": {
"line": 3,
"column": 92,
"offset": 207
}
},
"path": {
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Batman"
},
"attributes": {},
"data": {
"hash": 20
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 3,
"column": 92,
"offset": 207
},
"end": {
"line": 3,
"column": 99,
"offset": 214
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 3,
"column": 43,
"offset": 158
},
"end": {
"line": 3,
"column": 99,
"offset": 214
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 3,
"column": 99,
"offset": 214
},
"end": {
"line": 3,
"column": 100,
"offset": 215
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 116
},
"end": {
"line": 4,
"column": 1,
"offset": 216
}
},
"data": {
"hash": 9
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 2,
"offset": 217
},
"end": {
"line": 4,
"column": 3,
"offset": 218
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "underline",
"value": "Clark Kent",
"position": {
"start": {
"line": 4,
"column": 3,
"offset": 218
},
"end": {
"line": 4,
"column": 15,
"offset": 230
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 15,
"offset": 230
},
"end": {
"line": 4,
"column": 18,
"offset": 233
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 2,
"offset": 217
},
"end": {
"line": 4,
"column": 18,
"offset": 233
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 18,
"offset": 233
},
"end": {
"line": 4,
"column": 19,
"offset": 234
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 19,
"offset": 234
},
"end": {
"line": 4,
"column": 20,
"offset": 235
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "verbatim",
"value": "Kryptonian",
"position": {
"start": {
"line": 4,
"column": 20,
"offset": 235
},
"end": {
"line": 4,
"column": 32,
"offset": 247
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 32,
"offset": 247
},
"end": {
"line": 4,
"column": 33,
"offset": 248
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 19,
"offset": 234
},
"end": {
"line": 4,
"column": 33,
"offset": 248
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 33,
"offset": 248
},
"end": {
"line": 4,
"column": 34,
"offset": 249
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " M ",
"position": {
"start": {
"line": 4,
"column": 34,
"offset": 249
},
"end": {
"line": 4,
"column": 42,
"offset": 257
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 34,
"offset": 249
},
"end": {
"line": 4,
"column": 42,
"offset": 257
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 42,
"offset": 257
},
"end": {
"line": 4,
"column": 43,
"offset": 258
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 43,
"offset": 258
},
"end": {
"line": 4,
"column": 44,
"offset": 259
}
},
"data": {
"hash": 19
}
},
{
"type": "link",
"children": [
{
"type": "opening",
"element": "link",
"position": {
"start": {
"line": 4,
"column": 44,
"offset": 259
},
"end": {
"line": 4,
"column": 45,
"offset": 260
}
},
"data": {
"hash": 22
}
},
{
"type": "link.path",
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Superman",
"position": {
"start": {
"line": 4,
"column": 45,
"offset": 260
},
"end": {
"line": 4,
"column": 85,
"offset": 300
}
},
"data": {
"hash": 21
}
},
{
"type": "text",
"value": "Superman",
"position": {
"start": {
"line": 4,
"column": 86,
"offset": 301
},
"end": {
"line": 4,
"column": 94,
"offset": 309
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "link",
"position": {
"start": {
"line": 4,
"column": 95,
"offset": 310
},
"end": {
"line": 4,
"column": 96,
"offset": 311
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 4,
"column": 44,
"offset": 259
},
"end": {
"line": 4,
"column": 96,
"offset": 311
}
},
"path": {
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Superman"
},
"attributes": {},
"data": {
"hash": 20
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 4,
"column": 96,
"offset": 311
},
"end": {
"line": 4,
"column": 101,
"offset": 316
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 4,
"column": 43,
"offset": 258
},
"end": {
"line": 4,
"column": 101,
"offset": 316
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 4,
"column": 101,
"offset": 316
},
"end": {
"line": 4,
"column": 102,
"offset": 317
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 4,
"column": 1,
"offset": 216
},
"end": {
"line": 5,
"column": 1,
"offset": 318
}
},
"data": {
"hash": 9
}
},
{
"type": "table.row",
"children": [
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 2,
"offset": 319
},
"end": {
"line": 5,
"column": 3,
"offset": 320
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "italic",
"value": "Diana Prince",
"position": {
"start": {
"line": 5,
"column": 3,
"offset": 320
},
"end": {
"line": 5,
"column": 17,
"offset": 334
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 17,
"offset": 334
},
"end": {
"line": 5,
"column": 18,
"offset": 335
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 2,
"offset": 319
},
"end": {
"line": 5,
"column": 18,
"offset": 335
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 18,
"offset": 335
},
"end": {
"line": 5,
"column": 19,
"offset": 336
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 19,
"offset": 336
},
"end": {
"line": 5,
"column": 20,
"offset": 337
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"style": "code",
"value": "Amazonian",
"position": {
"start": {
"line": 5,
"column": 20,
"offset": 337
},
"end": {
"line": 5,
"column": 31,
"offset": 348
}
},
"data": {
"hash": 19
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 31,
"offset": 348
},
"end": {
"line": 5,
"column": 33,
"offset": 350
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 19,
"offset": 336
},
"end": {
"line": 5,
"column": 33,
"offset": 350
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 33,
"offset": 350
},
"end": {
"line": 5,
"column": 34,
"offset": 351
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " F ",
"position": {
"start": {
"line": 5,
"column": 34,
"offset": 351
},
"end": {
"line": 5,
"column": 42,
"offset": 359
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 34,
"offset": 351
},
"end": {
"line": 5,
"column": 42,
"offset": 359
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 42,
"offset": 359
},
"end": {
"line": 5,
"column": 43,
"offset": 360
}
},
"data": {
"hash": 44
}
},
{
"type": "table.cell",
"children": [
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 43,
"offset": 360
},
"end": {
"line": 5,
"column": 44,
"offset": 361
}
},
"data": {
"hash": 19
}
},
{
"type": "link",
"children": [
{
"type": "opening",
"element": "link",
"position": {
"start": {
"line": 5,
"column": 44,
"offset": 361
},
"end": {
"line": 5,
"column": 45,
"offset": 362
}
},
"data": {
"hash": 22
}
},
{
"type": "link.path",
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Wonder_Woman",
"position": {
"start": {
"line": 5,
"column": 45,
"offset": 362
},
"end": {
"line": 5,
"column": 89,
"offset": 406
}
},
"data": {
"hash": 21
}
},
{
"type": "text",
"value": "Wonder Woman",
"position": {
"start": {
"line": 5,
"column": 90,
"offset": 407
},
"end": {
"line": 5,
"column": 102,
"offset": 419
}
},
"data": {
"hash": 19
}
},
{
"type": "closing",
"element": "link",
"position": {
"start": {
"line": 5,
"column": 103,
"offset": 420
},
"end": {
"line": 5,
"column": 104,
"offset": 421
}
},
"data": {
"hash": 23
}
}
],
"position": {
"start": {
"line": 5,
"column": 44,
"offset": 361
},
"end": {
"line": 5,
"column": 104,
"offset": 421
}
},
"path": {
"protocol": "https",
"value": "https://en.wikipedia.org/wiki/Wonder_Woman"
},
"attributes": {},
"data": {
"hash": 20
}
},
{
"type": "text",
"value": " ",
"position": {
"start": {
"line": 5,
"column": 104,
"offset": 421
},
"end": {
"line": 5,
"column": 105,
"offset": 422
}
},
"data": {
"hash": 19
}
}
],
"position": {
"start": {
"line": 5,
"column": 43,
"offset": 360
},
"end": {
"line": 5,
"column": 105,
"offset": 422
}
},
"data": {
"hash": 10
}
},
{
"type": "table.columnSeparator",
"position": {
"start": {
"line": 5,
"column": 105,
"offset": 422
},
"end": {
"line": 5,
"column": 106,
"offset": 423
}
},
"data": {
"hash": 44
}
}
],
"position": {
"start": {
"line": 5,
"column": 1,
"offset": 318
},
"end": {
"line": 5,
"column": 107,
"offset": 424
}
},
"data": {
"hash": 9
}
}
],
"attributes": {},
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 107,
"offset": 424
}
},
"data": {
"hash": 8
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 5,
"column": 107,
"offset": 424
}
}
}
================================================
FILE: packages/orga/src/__tests__/table/with inline styles.org
================================================
| Name | Species | Gender | Role |
|----------------+--------------+--------+--------------|
| *Bruce Wayne* | +Bat+ Human | M | [[https://en.wikipedia.org/wiki/Batman][Batman]] |
| _Clark Kent_ | =Kryptonian= | M | [[https://en.wikipedia.org/wiki/Superman][Superman]] |
| /Diana Prince/ | ~Amazonian~ | F | [[https://en.wikipedia.org/wiki/Wonder_Woman][Wonder Woman]] |
================================================
FILE: packages/orga/src/index.ts
================================================
import {
defaultLexerOptions,
defaultParserOptions,
type LexerOptions,
type Options
} from './options.js'
import { parser as _parser, type Parser } from './parse/index.js'
import { parse as parseTimestamp } from './timestamp.js'
import { todoManager } from './todo.js'
import { tokenize as _tokenize, type Lexer } from './tokenize/index.js'
import type { Document, Settings } from './types.js'
export * from './types.js'
export { parseTimestamp, type Options as ParseOptions, type Parser }
export const tokenize = (
text: string,
options: Partial = {}
): Lexer => {
return _tokenize(text, { ...defaultLexerOptions, ...options })
}
export const parse = (
text: string,
options: Partial = {}
): Document => {
const parser = makeParser(text, options)
return parser.parse()
}
export function makeParser(text: string, options: Partial = {}) {
const { range, ..._options } = { ...defaultParserOptions, ...options }
const todo = todoManager(...getTodo(options.settings))
const start = range?.start
const lexer = tokenize(text, {
..._options,
todo,
range: start ? { start, end: Infinity } : undefined
})
return _parser(lexer, { ..._options, range })
}
export function getSettings(text: string): Settings {
// Stop when seeing a non-keyword, non-empty line.
const settings: Settings = {}
const lexer = tokenize(text)
let token = lexer.peek()
while (token) {
if (token.type === 'keyword') {
const k = (token.key as string).toLowerCase()
const v = (token.value as string).trim()
// Handle multiple values for the same key (like multiple #+todo: lines)
const existing = settings[k]
if (existing) {
if (Array.isArray(existing)) {
existing.push(v)
} else if (typeof existing === 'string') {
settings[k] = [existing, v]
}
} else {
settings[k] = v
}
lexer.eat()
} else if (token.type === 'newline' || token.type === 'emptyLine') {
lexer.eat()
} else {
// Hit non-keyword content - stop collecting
break
}
token = lexer.peek()
}
return settings
}
function getTodo(settings: Settings) {
const todo = settings?.todo
if (Array.isArray(todo)) return todo
if (typeof todo === 'string') return [todo]
return ['TODO DONE']
}
================================================
FILE: packages/orga/src/nodes.ts
================================================
let i = 0
export const nodeIdMap: Record = Object.freeze({
document: i++,
section: i++,
footnote: i++,
block: i++,
latex: i++,
drawer: i++,
planning: i++,
list: i++,
table: i++,
'table.row': i++,
'table.cell': i++,
'list.item': i++,
headline: i++,
paragraph: i++,
html: i++,
jsx: i++,
hr: i++,
newline: i++,
emptyLine: i++,
text: i++,
link: i++,
'link.path': i++,
opening: i++,
closing: i++,
'footnote.reference': i++,
stars: i++,
todo: i++,
priority: i++,
tags: i++,
'block.begin': i++,
'block.end': i++,
'drawer.begin': i++,
'drawer.end': i++,
'latex.begin': i++,
'latex.end': i++,
comment: i++,
keyword: i++,
'footnote.label': i++,
'planning.keyword': i++,
'planning.timestamp': i++,
'list.item.tag': i++,
'list.item.checkbox': i++,
'list.item.bullet': i++,
'table.hr': i++,
'table.columnSeparator': i++
})
================================================
FILE: packages/orga/src/options.ts
================================================
import type { Range } from 'text-kit'
import { defaultTodoManager, type TodoManager } from './todo.js'
import type { Settings } from './types.js'
export interface LexerOptions {
timezone: string
range?: Partial
todo: TodoManager
}
export interface ParserOptions {
flat: boolean
range?: Partial
settings?: Settings
}
export interface Options {
timezone: string
range?: Partial
settings?: Settings
flat: boolean
}
export const defaultParserOptions: ParserOptions = {
flat: false
}
export const defaultLexerOptions: LexerOptions = {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
todo: defaultTodoManager
}
export const defaultOptions: Options = {
timezone: defaultLexerOptions.timezone,
flat: defaultParserOptions.flat
}
================================================
FILE: packages/orga/src/parse/_parseSymbols.ts
================================================
import type { Primitive } from '../types.js'
import primitive from './_primitive.js'
export default (text: string): { [key: string]: Primitive } => {
let t = text
const result = {}
while (t.length > 0) {
const m = t.match(/^:\w+/)
if (!m) break
const key = m[0].substring(1)
t = t.slice(m[0].length)
const end = t.match(/\s(:\w+)/)
const index = end ? end.index + 1 : t.length
const value = t.substring(0, index).trim()
t = t.slice(index)
result[key] = primitive(value)
}
return result
}
================================================
FILE: packages/orga/src/parse/_primitive.ts
================================================
import type { Primitive } from '../types.js'
export default (value: string): Primitive => {
const num = Number(value)
if (!Number.isNaN(num)) return num
if (value.toLowerCase() === 'true') return true
if (value.toLowerCase() === 'false') return false
return value
}
================================================
FILE: packages/orga/src/parse/_utils.ts
================================================
const COMMON_IMAGE_EXTENSIONS = [
'apng',
'avif',
'gif',
'jpeg',
'jpg',
'jfif',
'pjpeg',
'pjp',
'png',
'svg',
'webp',
'bmp',
'ico',
'cur',
'tif',
'tiff'
]
export const isImage = (path: string) => {
const ext = path.toLowerCase().split('.').pop()
return COMMON_IMAGE_EXTENSIONS.includes(ext)
}
================================================
FILE: packages/orga/src/parse/block.ts
================================================
import type { BlockBegin, BlockEnd } from '../types.js'
import type { Action, Handler } from './index.js'
const block: Action = (begin: BlockBegin, ctx): Handler => {
ctx.save()
const contentStart = begin.position.end
const blockName = begin.name.toLowerCase()
const block = ctx.enter({
type: 'block',
name: begin.name,
params: begin.params,
value: '',
attributes: { ...ctx.attributes },
children: []
})
ctx.lexer.eat()
/*
* find the indentation of the block and apply it to
* the rest of the block.
*
* The indentation of the first non-blank line is used as standard.
* The following lines use the lesser one between its own
* indentation and the standard. Leading and trailing blank lines
* are omitted.
*/
const align = (content: string) => {
let indent = -1
return content
.trimEnd()
.split('\n')
.map((line) => {
const _indent = line.search(/\S/)
if (indent === -1) {
indent = _indent
}
if (indent === -1) return ''
let result = line.substring(Math.min(_indent, indent))
// remove escaping char ,
if (block.name.toLowerCase() === 'src' && block.params[0] === 'org') {
result = result.replace(/^(\s*),/, '$1')
}
return result
})
.join('\n')
.trim()
}
return {
name: 'block',
rules: [
{
test: 'block.end',
action: (token: BlockEnd, context) => {
const lexer = context.lexer
if (token.name.toLowerCase() !== blockName) return 'next'
block.value = align(
lexer.substring({
start: contentStart,
end: token.position.start
})
)
lexer.eat()
context.exit('block')
return 'break'
}
},
{
test: ['stars', 'EOF'],
action: (_, context) => {
context.restore()
context.lexer.modify((t) => ({
type: 'text',
value: context.lexer.substring(t.position),
position: t.position
}))
return 'break'
}
},
{ test: 'newline', action: (_, { discard }) => discard() },
{ test: /./, action: (_, { consume }) => consume() }
]
}
}
export default block
================================================
FILE: packages/orga/src/parse/context.ts
================================================
import type { Node, Parent, Point } from 'unist'
import { nodeIdMap } from '../nodes.js'
import type { ParserOptions } from '../options.js'
import type { Lexer } from '../tokenize/index.js'
import {
type Attributes,
type Document,
isSection,
type Properties
} from '../types.js'
import type { Predicate } from './index.js'
import { not, test } from './index.js'
/**
* Exclude `todo` from settings when initializing document properties.
* The `todo` setting is only used for TodoManager initialization (for tokenization),
* not for document properties. Properties.todo should be discovered fresh during parsing.
*/
function initialProperties(settings: Properties | undefined): Properties {
if (!settings) return {}
const { todo, ...rest } = settings
return rest
}
interface Snapshot {
stack: Parent[]
savePoint: number
level: number
attributes: Attributes
}
export interface Context {
// control
// -
enter: (node: N) => N
exit: (predicate: Predicate, strict?: boolean) => Parent | void
attach: (node: Node) => void
save: () => void
restore: () => void
addProp: (key: string, value: string) => void
// syntactic sugar
// -
exitTo: (predicate: Predicate) => void
exitAll: (predicate: Predicate) => void
/** shorthand for lexer.eat and push it **/
consume: () => void
/** shorthand for lexer.eat **/
discard: () => void
within: (predicate: Predicate) => boolean
// state
attributes: Attributes
readonly parent: Parent
readonly level: number
readonly tree: Document
readonly lexer: Lexer
readonly state: string
readonly options: ParserOptions
}
function point(d: Point): Point {
return { ...d }
}
export function createContext(lexer: Lexer, options: ParserOptions): Context {
let stack: Parent[] = []
let snapshot: Snapshot | undefined
function enter(node: N): N {
const start = lexer.peek()?.position?.start ||
lexer.peek(-1)?.position?.end || { line: 1, column: 1, offset: 0 }
// @ts-expect-error will add the end later
node.position = { start: point(start) }
stack.push(node)
return node
}
const tree = enter({
type: 'document',
properties: initialProperties(options.settings),
children: []
}) as Document
const pop = () => {
const node = stack.pop()
const end = lexer.peek()?.position?.start ||
lexer.peek(-1)?.position?.end || { line: 1, column: 1, offset: 0 }
node.position.end = point(end)
if (!node) {
throw new Error('unexpected empty stack')
}
// attach to tree
if (stack.length > 0) {
attach(node)
}
return node
}
function exit(predicate: Predicate, strict = true) {
if (stack.length === 0) return // never exit the root
const last = stack[stack.length - 1]
if (test(last, predicate)) {
return pop()
}
if (strict) {
throw new Error(
`
can not strictly exit ${predicate},
actual: ${last.type}
location: line: ${last.position.start.line}, column: ${last.position.start.column}
`.trim()
)
}
}
function exitTo(predicate: Predicate) {
exitAll(not(predicate))
}
function exitAll(predicate: Predicate) {
if (exit(predicate, false)) {
exitAll(predicate)
}
}
function getLevel(): number {
let index = stack.length - 1
while (index > 0) {
const node = stack[index]
if (isSection(node)) {
return node.level
}
index -= 1
}
return 0
}
/**
* attach a node to the current tree, adding data.hash
*/
function attach(node: Node) {
if (!node) return
if (stack.length === 0) {
throw new Error('unexpected empty stack')
}
const parent = stack[stack.length - 1]
node.data = { hash: hash(node.type) }
parent.children.push(node)
}
return {
options,
attributes: {},
enter,
exit,
exitAll,
exitTo,
attach,
addProp: function (key, value) {
const k = key.toLowerCase().trim()
const v = value.trim()
const existing = tree.properties[k]
if (existing) {
if (Array.isArray(existing)) {
existing.push(v)
}
if (typeof existing === 'string') {
tree.properties[k] = [existing, v]
}
} else {
tree.properties[k] = v
}
},
consume: function () {
attach(lexer.eat())
},
discard: function () {
lexer.eat()
},
within: function (predicate: Predicate) {
return test(this.parent, predicate)
},
save: function () {
const level = this.level
const attributes = this.attributes
snapshot = {
stack: [...stack],
level,
attributes: { ...attributes },
savePoint: lexer.save()
}
},
restore: function () {
if (snapshot === undefined) return
this.attributes = { ...snapshot.attributes }
stack = [...snapshot.stack]
lexer.restore(snapshot.savePoint)
},
get parent() {
return stack[stack.length - 1]
},
get tree() {
return tree
},
get lexer() {
return lexer
},
get level() {
return getLevel()
},
get state() {
const token = lexer.peek()
const lines = [`lexer: ${lexer.save()}`]
lines.push(`token: ${token ? token.type : 'EOF'}`)
if (token) {
lines.push(`content: ${lexer.substring(token.position)}`)
}
lines.push(`stack: ${stack.map((n) => n.type).join(' > ')}`)
return lines.join('\n')
}
}
function hash(type: string, value = 0) {
const typeHash = nodeIdMap[type]
let baseHash = 0
if (stack.length > 0) {
baseHash = stack[stack.length - 1].data?.hash ?? 0
}
return (baseHash + (baseHash << 8) + typeHash + (value << 4)) | 0
}
}
================================================
FILE: packages/orga/src/parse/drawer.ts
================================================
import type { DrawerBegin, Section } from '../types.js'
import type { Action } from './index.js'
const drawer: Action = (begin: DrawerBegin, context) => {
context.save()
const drawer = context.enter({
type: 'drawer',
name: begin.name,
value: '',
children: []
})
context.consume()
const contentStart = begin.position.end
return {
name: 'drawer',
rules: [
{
test: ['stars', 'EOF'],
action: (_, context) => {
context.restore()
context.lexer.modify((t) => ({
type: 'text',
value: context.lexer.substring(t.position),
position: t.position
}))
return 'break'
}
},
{
test: 'drawer.end',
action: (token, context) => {
context.consume()
drawer.value = context.lexer.substring({
start: contentStart,
end: token.position.start
})
context.exit('drawer')
context.lexer.eat('newline')
if (drawer.name.toLowerCase() === 'properties') {
const section = context.parent as Section
section.properties = drawer.value
.split('\n')
.reduce((accu, current) => {
const m = current.match(/\s*:(.+?):\s*(.+)\s*$/)
if (m) {
return { ...accu, [m[1].toLowerCase()]: m[2] }
}
return accu
}, section.properties)
}
return 'break'
}
},
{
test: /.*/,
action: (_, { consume }) => consume()
}
]
}
}
export default drawer
================================================
FILE: packages/orga/src/parse/footnote.ts
================================================
import type { FootnoteLabel } from '../types.js'
import type { Action } from './index.js'
const Footnote: Action = (token: FootnoteLabel, { enter, exitTo, consume }) => {
exitTo('document')
enter({
type: 'footnote',
label: token.label,
children: []
})
consume()
}
export default Footnote
================================================
FILE: packages/orga/src/parse/headline.ts
================================================
import type { Headline, Priority, Stars, Tags, Todo } from '../types.js'
import { isPhrasingContent } from '../utils.js'
import type { Action } from './index.js'
import phrasingContent from './phrasing.js'
const headline: Action = (token: Stars, context) => {
const { enter } = context
const headline: Headline = enter({
type: 'headline',
actionable: false,
children: [],
level: token.level || context.level
})
return {
name: 'headline',
rules: [
{
test: ['newline', 'EOF'],
action: (_, { exit, discard }) => {
discard()
exit(headline.type)
return 'break'
}
},
{
test: 'todo',
action: (token: Todo) => {
headline.keyword = token.keyword
headline.actionable = token.actionable
return 'next'
}
},
{
test: 'tags',
action: (token: Tags) => {
headline.tags = token.tags
return 'next'
}
},
{
test: 'priority',
action: (token: Priority) => {
headline.priority = token.value
return 'next'
}
},
{
test: isPhrasingContent,
action: phrasingContent
},
{
test: /.*/,
action: (_, { attach, lexer }) => {
attach(lexer.eat())
}
}
]
}
}
export default headline
================================================
FILE: packages/orga/src/parse/index.ts
================================================
import type { Node } from 'unist'
import type { ParserOptions } from '../options.js'
import type { Lexer } from '../tokenize/index.js'
import type { Document, Parent, Settings, Token } from '../types.js'
import { isPhrasingContent } from '../utils.js'
import block from './block.js'
import { type Context, createContext } from './context.js'
import footnote from './footnote.js'
import keyword from './keyword.js'
import latex from './latex.js'
import list from './list.js'
import paragraph from './paragraph.js'
import section from './section.js'
import table from './table.js'
export type Parse = (lexer: Lexer) => Parent | undefined
/*
* break: pop handler stack, go to next handler
* next: go to next rule in the current handler
* finish | void: finish current handler, skip the upcoming rules
*/
type FlowControl = 'break' | 'next' | 'finish'
export type Action = (
token: Token,
context: Context
) => FlowControl | Handler | void
export type Predicate = string | 'EOF' | RegExp | ((token: Node) => boolean)
export function test(node: Node, predicate: Predicate) {
return toFunc(predicate)(node)
}
export function not(test: Predicate): Predicate {
return (token) => !toFunc(test)(token)
}
export function toFunc(test: Predicate): (token: Node) => boolean {
if (typeof test === 'function') {
return (token) => token && test(token)
}
if (test === 'EOF') {
return (token) => {
return token === undefined
}
}
if (typeof test === 'string') {
return (token) => token && test === token.type
}
return (token) => token && test.test(token.type)
}
type Rule = { test: Predicate | Predicate[]; action: Action | Handler }
export interface Handler {
name: string
rules: Rule[]
eof?: (context: Context) => void
}
const main: Handler = {
name: 'main',
rules: [
{
test: 'emptyLine',
action: function (_, context) {
const { consume, exit } = context
context.attributes = {}
exit('footnote', false)
consume()
}
},
{ test: 'newline', action: (_, { discard }) => discard() },
{ test: 'stars', action: section },
{ test: 'keyword', action: keyword },
{ test: 'list.item.bullet', action: list },
{ test: 'block.begin', action: block },
{ test: 'latex.begin', action: latex },
{ test: /^table\./, action: table },
{
test: 'hr',
action: (token, { lexer, attach }) => {
lexer.eat()
attach(token)
}
},
{ test: isPhrasingContent, action: paragraph },
{ test: 'footnote.label', action: footnote },
// catch all
{
test: /.*/,
action: (_, { lexer }) => {
lexer.eat()
}
},
{ test: 'EOF', action: () => 'break' }
]
}
export interface Parser {
advance: () => Document | number
parse: () => Document
finish: () => Document
readonly settings?: Settings
}
export function parser(lexer: Lexer, options: ParserOptions): Parser {
const context = createContext(lexer, options)
const end = lexer.toOffset(options.range?.end || Infinity)
const handlerStack: Handler[] = [main]
let lexerLocation = lexer.save()
let maxStaleIterations = 10
return {
advance,
parse() {
for (;;) {
const tree = advance()
if (typeof tree === 'number') continue
return tree
}
},
finish,
get settings() {
return context.tree.properties
}
}
function handler() {
return handlerStack.length > 0
? handlerStack[handlerStack.length - 1]
: undefined
}
function advance() {
if (!handler() && lexer.now >= end) {
return finish()
}
// prevent infinit loop
if (maxStaleIterations === 0) {
throw new Error(`it's stuck. \n${context.state}`)
}
let nothingMatches = true
for (const { test: _test, action } of handler().rules) {
const token = lexer.peek()
const predicates = Array.isArray(_test) ? _test : [_test]
if (!predicates.some((p) => test(token, p))) continue
nothingMatches = false
if (typeof action !== 'function') {
handlerStack.push(action)
return advance()
}
const control = action(token, context)
if (typeof control === 'object') {
handlerStack.push(control)
return advance()
}
if (control === 'break') {
handlerStack.pop()
// if (handlerStack.length === 0) {
// throw new Error('can not pop the root handler')
// }
// return the offset if the block is finished
if (lexer.peek() && context.parent.type === 'document') {
return lexer.peek().position.start.offset
}
return advance()
}
if (control === 'next') {
continue
}
break
}
if (nothingMatches) {
handlerStack.pop()
return advance()
}
if (lexer.save() === lexerLocation) {
maxStaleIterations -= 1
} else {
lexerLocation = lexer.save()
maxStaleIterations = 10
}
return advance()
}
function finish() {
context.exitTo('document')
context.exit('document')
// algin the end position
context.tree.position.end = lexer.toPoint(lexer.now)
return context.tree
}
}
================================================
FILE: packages/orga/src/parse/keyword.ts
================================================
import type { HTML, JSX, Keyword, Primitive } from '../types.js'
import parseSymbols from './_parseSymbols.js'
import _primitive from './_primitive.js'
import type { Action } from './index.js'
const AFFILIATED_KEYWORDS = ['caption', 'header', 'name', 'plot', 'results']
const keyword: Action = (token: Keyword, context) => {
const { attach, lexer, addProp } = context
const key = token.key.toLowerCase()
const { value } = token
if (key === 'html') {
attach({ type: 'html', value, position: token.position } as HTML)
} else if (key === 'jsx') {
attach({ type: 'jsx', value, position: token.position } as JSX)
} else {
if (AFFILIATED_KEYWORDS.includes(key)) {
context.attributes[key] = _primitive(value)
} else if (key.startsWith('attr_')) {
context.attributes[key] = {
...(context.attributes[key] as { [key: string]: Primitive }),
...parseSymbols(value)
}
} else {
addProp(key, value)
}
if (key === 'todo') {
lexer.todo.add(value)
}
attach(token)
}
// if (AFFILIATED_KEYWORDS.includes(key)) {
// context.attributes[key] = _primitive(value)
// } else if (key.startsWith('attr_')) {
// context.attributes[key] = {
// ...(context.attributes[key] as { [key: string]: Primitive }),
// ...parseSymbols(value),
// }
// } else if (key === 'todo') {
// lexer.addInBufferTodoKeywords(value)
// } else if (key === 'html') {
// push({ type: 'html', value } as HTML)
// } else if (key === 'jsx') {
// push({ type: 'jsx', value } as JSX)
// } else {
// addProp(key, value)
// }
lexer.eat()
}
export default keyword
// export default (context: Context) => {
// const { push, lexer } = context
// const token = lexer.peek()
// if (token.type !== 'keyword') return
// lexer.eat()
// }
================================================
FILE: packages/orga/src/parse/latex.ts
================================================
import type { LatexBegin, LatexEnd } from '../types.js'
import type { Action, Handler } from './index.js'
const latex: Action = (begin: LatexBegin, context): Handler => {
context.save()
const contentStart = begin.position.start
const envName = begin.name.toLowerCase()
const latexBlock = context.enter({
type: 'latex',
name: begin.name,
value: '',
children: []
})
context.attach(context.lexer.eat())
/*
* find the indentation of the block and apply it to
* the rest of the block.
*
* The indentation of the first non-blank line is used as standard.
* The following lines use the lesser one between its own
* indentation and the standard. Leading and trailing blank lines
* are omitted.
*/
const align = (content: string) => {
let indent = -1
return content
.trimEnd()
.split('\n')
.map((line) => {
const _indent = line.search(/\S/)
if (indent === -1) {
indent = _indent
}
if (indent === -1) return ''
const result = line.substring(Math.min(_indent, indent))
return result
})
.join('\n')
.trim()
}
return {
name: 'latex',
rules: [
{
test: 'latex.end',
action: (token: LatexEnd, context) => {
if (token.name.toLowerCase() !== envName) return 'next'
latexBlock.value = align(
context.lexer.substring({
start: contentStart,
end: token.position.end
})
)
context.attach(context.lexer.eat())
context.lexer.eat('newline')
context.exit('latex')
return 'break'
}
},
{
test: ['stars', 'EOF'],
action: (_, context) => {
context.restore()
context.lexer.modify((t) => ({
type: 'text',
value: context.lexer.substring(t.position),
position: t.position
}))
return 'break'
}
},
{ test: /./, action: (_, context) => context.attach(context.lexer.eat()) }
]
}
}
export default latex
================================================
FILE: packages/orga/src/parse/list.ts
================================================
import type { ListItem, ListItemBullet, ListItemTag } from '../types.js'
import { isPhrasingContent } from '../utils.js'
import type { Action, Handler } from './index.js'
import phrasingContent from './phrasing.js'
const listItem: Action = (token: ListItemBullet, { enter, consume }) => {
const item: ListItem = enter({
type: 'list.item',
indent: token.indent,
children: []
})
consume()
return {
name: 'list item',
rules: [
{
test: 'emptyLine',
action: (_, { exit, consume }) => {
consume()
exit(item.type)
return 'break'
}
},
{ test: 'newline', action: (_, { consume }) => consume() },
{
test: 'list.item.tag',
action: (token: ListItemTag, { consume }) => {
item.tag = token.value
consume()
}
},
{
test: 'list.item.checkbox',
action: (_, { consume }) => {
consume()
}
},
{ test: isPhrasingContent, action: phrasingContent },
{
test: /.*/,
action: (_, { exit }) => {
exit('list.item')
return 'break'
}
}
]
}
}
const list: Action = (token: ListItemBullet, context) => {
context.enter({
type: 'list',
indent: token.indent,
ordered: token.ordered,
children: [],
attributes: context.attributes
})
context.attributes = {}
const indent = token.indent
const handler: Handler = {
name: 'list',
rules: [
{
test: 'stars',
action: (_, { exit }) => {
exit('list')
return 'break'
}
},
{
test: ['emptyLine', 'newline'],
action: (_, { exit, consume }) => {
consume()
exit('list')
return 'break'
}
},
{
test: 'list.item.bullet',
action: (token: ListItemBullet, context) => {
const { exit } = context
if (indent > token.indent) {
exit('list')
return 'break'
} else if (indent === token.indent) {
return listItem(token, context)
} else {
return list(token, context)
}
}
},
{
test: /.*/,
action: (_, { exit }) => {
exit('list')
return 'break'
}
}
]
}
return handler
}
export default list
================================================
FILE: packages/orga/src/parse/paragraph.ts
================================================
import { isParagraph, type Opening, type PhrasingContent } from '../types.js'
import { clone, isPhrasingContent } from '../utils.js'
import { isImage } from './_utils.js'
import type { Context } from './context.js'
import type { Action } from './index.js'
import phrasingContent from './phrasing.js'
const isWhitespaces = (node: PhrasingContent) => {
return (
(node.type === 'text' && node.value.trim().length === 0) ||
node.type === 'newline' ||
node.type === 'emptyLine'
)
}
const paragraph: Action = () => {
const makeSureParagraph = (context: Context) => {
const parent = context.parent
if (parent.type === 'paragraph') return
context.save()
context.enter({
type: 'paragraph',
children: [],
attributes: clone(context.attributes)
})
context.attributes = {}
}
const exitPragraph = (context: Context) => {
const paragraph = context.parent
if (!isParagraph(paragraph)) return
if (
paragraph.children.length === 0 ||
paragraph.children.every(isWhitespaces)
) {
context.restore()
} else {
// TODO: should we do this?
// exitTo('paragraph')
context.exit('paragraph')
}
}
return {
name: 'paragraph',
rules: [
{
test: 'emptyLine',
action: (_, context) => {
context.consume()
exitPragraph(context)
return 'break'
}
},
{
test: 'newline',
action: (_, { consume }) => consume()
},
{
test: 'opening',
action: (token: Opening, context) => {
if (token.element === 'link') {
const next = context.lexer.peek(1)
if (next.type === 'link.path' && isImage(next.value)) {
exitPragraph(context)
return phrasingContent
}
}
makeSureParagraph(context)
return phrasingContent
}
},
{
test: isPhrasingContent,
action: (_, context) => {
makeSureParagraph(context)
return phrasingContent
}
},
// catch all
{
test: /.*/,
action: (_, context) => {
exitPragraph(context)
return 'break'
}
}
]
}
}
export default paragraph
================================================
FILE: packages/orga/src/parse/phrasing.ts
================================================
import {
type Closing,
type FootnoteLabel,
isFootnoteReference,
isLink,
type LinkPath,
type Opening
} from '../types.js'
import type { Handler } from './index.js'
const phrasingContent: Handler = {
name: 'inline',
rules: [
{
test: 'opening',
action: (token: Opening, { enter, consume }) => {
enter({
type: token.element,
children: []
})
consume()
}
},
{
test: 'closing',
action: (token: Closing, { exit, consume }) => {
consume()
exit(token.element)
}
},
{
test: 'link.path',
action: (token: LinkPath, context) => {
const { parent, consume, attributes } = context
if (!isLink(parent)) {
throw new Error('expect parent to be link')
}
parent.path = {
protocol: token.protocol,
value: token.value,
search: token.search
}
parent.attributes = attributes
context.attributes = {}
consume()
}
},
{
test: 'footnote.label',
action: (token: FootnoteLabel, { parent, consume }) => {
if (!isFootnoteReference(parent)) {
throw new Error('expect parent to be footnote reference')
}
parent.label = token.label
consume()
}
},
{
test: 'text',
action: (_, { consume }) => {
consume()
}
}
]
}
export default phrasingContent
================================================
FILE: packages/orga/src/parse/planning.ts
================================================
import type { PlanningKeyword } from '../types.js'
import drawer from './drawer.js'
import type { Handler } from './index.js'
const planning: Handler = {
name: 'planning',
rules: [
{
test: 'planning.keyword',
action: (keyword: PlanningKeyword, context) => {
const { lexer, enter, attach, exit } = context
const { eat, eatAll, peek } = lexer
const timestamp = peek(1)
if (!timestamp || timestamp.type !== 'planning.timestamp') {
return 'break'
}
enter({
type: 'planning',
keyword: keyword.value,
timestamp: timestamp.value,
children: []
})
attach(eat()) // keyword
attach(eat()) // timestamp
exit('planning')
if (eatAll('newline') > 1) {
return 'break'
}
}
},
{
test: 'drawer.begin',
action: (token, context) => {
return drawer(token, context)
}
},
{
test: /.*/,
action: () => {
return 'break'
}
}
]
}
export default planning
================================================
FILE: packages/orga/src/parse/section.ts
================================================
import type { Stars } from '../types.js'
import drawer from './drawer.js'
import headline from './headline.js'
import type { Action } from './index.js'
import planning from './planning.js'
const section: Action = (token: Stars, context) => {
const {
enter,
exit,
exitTo,
options: { flat }
} = context
// stars break footnote
exit('footnote', false)
if (!flat) {
const level = token.level
if (level <= context.level) {
exitTo('section')
exit('section')
return
}
enter({
type: 'section',
level: level,
properties: {},
children: []
})
}
let headlineProcessed = false
return {
name: 'section',
rules: [
{
test: 'stars',
action: (token: Stars, context) => {
if (headlineProcessed) return 'break'
headlineProcessed = true
return headline(token, context)
}
},
{
test: 'planning.keyword',
action: planning
},
{
test: 'drawer.begin',
action: drawer
}
]
}
}
export default section
================================================
FILE: packages/orga/src/parse/table.ts
================================================
import { isPhrasingContent } from '../utils.js'
import type { Action } from './index.js'
import phrasingContent from './phrasing.js'
const tableCell: Action = (_, { enter }) => {
enter({
type: 'table.cell',
children: []
})
return {
name: 'table cell',
rules: [
{
test: ['newline', 'table.columnSeparator'],
action: (_, { exit }) => {
exit('table.cell')
return 'break'
}
},
{
test: isPhrasingContent,
action: phrasingContent
},
{
test: /.*/,
action: (_, { exit }) => {
exit('table.cell')
return 'break'
}
}
]
}
}
const tableRow: Action = (_, { enter, lexer }) => {
enter({
type: 'table.row',
children: []
})
// consume()
lexer.eat()
return {
name: 'table row',
rules: [
{
test: 'newline',
action: (_, { exit, discard }) => {
discard()
exit('table.row')
return 'break'
}
},
{ test: 'table.columnSeparator', action: (_, { consume }) => consume() },
{ test: isPhrasingContent, action: tableCell }
]
}
}
const table: Action = (_, context) => {
context.enter({
type: 'table',
children: [],
attributes: {}
})
return {
name: 'table',
rules: [
{ test: 'table.columnSeparator', action: tableRow },
{ test: 'table.hr', action: (_, context) => context.consume() },
{ test: 'newline', action: (_, context) => context.discard() },
{
test: /.*/,
action: (_, context) => {
context.exitTo('table')
context.exit('table')
return 'break'
}
}
]
}
}
export default table
================================================
FILE: packages/orga/src/position.ts
================================================
import type { Point, Position } from 'unist'
export const isEqual = (p1: Point, p2: Point) => {
return p1.line === p2.line && p1.column === p2.column
}
export const isGreaterOrEqual = (p1: Point, p2: Point) => {
return isEqual(p1, p2) || before(p1)(p2)
}
const compare = (p1: Point, p2: Point): boolean => {
if (p1.line > p2.line) return true
if (p1.line === p2.line && p1.column > p2.column) return true
return false
}
export const after = (p1: Point) => (p2: Point) => {
return compare(p2, p1)
}
export const before = (p1: Point) => (p2: Point) => {
return compare(p1, p2)
}
export const isEmpty = (position: Position) => {
return !position || isEqual(position.start, position.end)
}
================================================
FILE: packages/orga/src/timestamp.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import { parse } from './timestamp'
const p = (text: string) => {
return parse(text, { timezone: 'Pacific/Auckland' })
}
describe('timestamp', () => {
it('can parse timestamps', () => {
assert.deepEqual(p('[2021-04-24 Sat]'), {
date: new Date('2021-04-23T12:00:00.000Z'),
end: undefined
})
assert.deepEqual(p('<2021-04-24 Sat>'), {
date: new Date('2021-04-23T12:00:00.000Z'),
end: undefined
})
assert.deepEqual(p('<2021-04-24 Sat 19:15>'), {
date: new Date('2021-04-24T07:15:00.000Z'),
end: undefined
})
assert.deepEqual(p('<2021-04-24 Sat 19:15-22:00>'), {
date: new Date('2021-04-24T07:15:00.000Z'),
end: new Date('2021-04-24T10:00:00.000Z')
})
assert.deepEqual(p('<2019-08-19 Mon>--<2019-08-20 Tue>'), {
date: new Date('2019-08-18T12:00:00.000Z'),
end: new Date('2019-08-19T12:00:00.000Z')
})
})
})
================================================
FILE: packages/orga/src/timestamp.ts
================================================
import { zonedTimeToUtc } from 'date-fns-tz'
import { read } from 'text-kit'
import type { Timestamp } from './types.js'
export const parse = (
input: string,
{ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone } = {}
): Timestamp | undefined => {
const { match, eat, getChar, jump } = read(input)
eat('whitespaces')
const timestamp = () => {
// opening
const opening = eat(/[<[]/g)
if (!opening) return
const _active = opening.value === '<'
// date
const { value: _date } = eat(/\d{4}-\d{2}-\d{2}/)
let date = _date
eat('whitespaces')
let end: string | undefined
// day
const { value: _day } = eat(/[a-zA-Z]+/)
eat('whitespaces')
// time
const time = match(/(\d{2}:\d{2})(?:-(\d{2}:\d{2}))?/)
if (time) {
date = `${_date} ${time.result[1]}`
if (time.result[2]) {
end = `${_date} ${time.result[2]}`
}
jump(time.position.end)
}
// closing
const closing = getChar()
if (
(opening.value === '[' && closing === ']') ||
(opening.value === '<' && closing === '>')
) {
eat('char')
return {
date: zonedTimeToUtc(date, timezone),
end: end ? zonedTimeToUtc(end, timezone) : undefined
}
}
// opening closing does not match
}
const ts = timestamp()
if (!ts) return
if (!ts.end) {
const doubleDash = eat(/--/)
if (doubleDash) {
const end = timestamp()
if (end) {
ts.end = end.date
}
}
}
return ts
}
================================================
FILE: packages/orga/src/todo.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import { parseTodoKeywords } from './todo.ts'
describe('todo manager', () => {
it('works', () => {
const t = parseTodoKeywords('TODO NEXT | DONE')
assert.deepEqual(t.keywords, ['TODO', 'NEXT', 'DONE'])
assert.equal(t.next('TODO'), 'NEXT')
assert.equal(t.next('NEXT'), 'DONE')
assert.equal(t.next('DONE'), 'TODO')
assert.equal(t.next('TODO', true), 'DONE')
assert.equal(t.next('NEXT', true), 'TODO')
assert.equal(t.next('DONE', true), 'NEXT')
assert.equal(t.next('MISSING'), undefined)
assert.equal(t.actionable('TODO'), true)
assert.equal(t.actionable('NEXT'), true)
assert.equal(t.actionable('DONE'), false)
assert.equal(t.actionable('MISSING'), false)
})
it('last one as done without |', () => {
const t = parseTodoKeywords('TODO NEXT DONE')
assert.deepEqual(t.keywords, ['TODO', 'NEXT', 'DONE'])
assert.equal(t.next('TODO'), 'NEXT')
assert.equal(t.next('NEXT'), 'DONE')
assert.equal(t.next('DONE'), 'TODO')
assert.equal(t.next('TODO', true), 'DONE')
assert.equal(t.next('NEXT', true), 'TODO')
assert.equal(t.next('DONE', true), 'NEXT')
assert.equal(t.next('MISSING'), undefined)
assert.equal(t.actionable('TODO'), true)
assert.equal(t.actionable('NEXT'), true)
assert.equal(t.actionable('DONE'), false)
assert.equal(t.actionable('MISSING'), false)
})
})
================================================
FILE: packages/orga/src/todo.ts
================================================
export interface TodoKeywordSet {
next: (value: string, reverse?: boolean) => string | undefined
actionable: (value: string) => boolean
includes: (value: string) => boolean
readonly keywords: string[]
}
export const defaultTodoManager = todoManager('TODO DONE')
export interface TodoManager extends TodoKeywordSet {
add: (todo: string) => void
}
/**
* A simple todo list manager that allows cycling through keywords.
* Keywords before the pipe '|' are considered actionable.
* Keywords after the pipe are considered non-actionable.
*
* Example usage:
* todo('todo doing | done cancelled')
* // Cycles through: todo -> doing -> done -> cancelled -> todo ...
*
* @param value A string of space-separated keywords with an optional pipe '|'.
*/
export function parseTodoKeywords(value: string): TodoKeywordSet {
const keywords = value.trim().split(' ').filter(Boolean)
let divider = keywords.indexOf('|')
if (divider === -1) divider = keywords.length - 1
return {
next,
actionable,
includes: (v: string) => keywords.includes(v),
get keywords() {
return keywords.filter((k) => k !== '|')
}
}
/**
* Get the next keyword in the list, circularly.
* @returns The next keyword or undefined if not found.
*/
function next(value: string, reverse = false) {
const index = keywords.indexOf(value)
if (index === -1) return undefined
const nextIndex = _next(index, reverse)
return keywords[nextIndex]
}
function _next(idx: number, reverse: boolean) {
const offset = reverse ? -1 : 1
let nextIndex = idx + offset
if (nextIndex < 0) nextIndex = keywords.length - 1
if (nextIndex >= keywords.length) nextIndex = 0
// skip the divider
if (keywords[nextIndex] === '|') {
return _next(nextIndex, reverse)
}
return nextIndex
}
function actionable(value: string) {
const index = keywords.indexOf(value)
if (index !== -1 && index < divider) return true
return false
}
}
export function todoManager(...keywords: string[]): TodoManager {
const todos: TodoKeywordSet[] = keywords.map(parseTodoKeywords)
return {
next,
actionable,
includes,
add,
get keywords() {
return todos.flatMap((t) => t.keywords)
}
}
function next(value: string, reverse = false) {
for (const todoSet of todos) {
if (todoSet.includes(value)) {
const nextKeyword = todoSet.next(value, reverse)
if (nextKeyword) return nextKeyword
}
}
return undefined
}
function actionable(value: string) {
for (const todoSet of todos) {
if (todoSet.includes(value)) {
return todoSet.actionable(value)
}
}
return false
}
function includes(value: string) {
for (const todoSet of todos) {
if (todoSet.includes(value)) {
return true
}
}
return false
}
function add(todo: string) {
todos.push(parseTodoKeywords(todo))
}
}
================================================
FILE: packages/orga/src/tokenize/__tests__/debug.ts
================================================
import { inspect } from 'node:util'
import chalk from 'chalk'
import { read } from 'text-kit'
import { tokenize } from '../index'
export default (text: string): void => {
const { substring } = read(text)
const tokens = tokenize(text).all()
const data = tokens.map((token) => ({
...token,
_content: substring(token.position)
}))
const lines = [
chalk.red('** DEBUG **'),
chalk.red('> text:'),
chalk.gray(text),
chalk.red('> tokens:')
].join('\n')
console.log(lines, inspect(data, false, null, true))
// console.log(inspect(data, false, null, true))
}
================================================
FILE: packages/orga/src/tokenize/__tests__/tok.ts
================================================
import { read } from 'text-kit'
import { defaultLexerOptions, type LexerOptions } from '../../options'
import { tokenize } from '../index'
export default (text: string, options: Partial = {}) => {
const { substring } = read(text)
const tokens = tokenize(text, { ...defaultLexerOptions, ...options }).all()
return tokens.map(({ position, ...token }) => ({
...token,
_text: position ? substring(position.start, position.end) : ''
}))
}
================================================
FILE: packages/orga/src/tokenize/blank.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize blanks', () => {
it('could handle blanks', () => {
assert.deepEqual(tokenize(''), [])
assert.deepEqual(tokenize(' '), [
{
_text: ' ',
type: 'emptyLine'
}
])
assert.deepEqual(tokenize(' '), [
{
_text: ' ',
type: 'emptyLine'
}
])
assert.deepEqual(tokenize('\t'), [
{
_text: '\t',
type: 'emptyLine'
}
])
assert.deepEqual(tokenize(' \t'), [
{
_text: ' \t',
type: 'emptyLine'
}
])
assert.deepEqual(tokenize('\t '), [
{
_text: '\t ',
type: 'emptyLine'
}
])
assert.deepEqual(tokenize(' \t '), [
{
_text: ' \t ',
type: 'emptyLine'
}
])
})
it('knows these are not blanks', () => {
assert.deepEqual(tokenize(' a '), [
{
_text: ' a ',
type: 'text',
value: ' a '
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/block.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize block', () => {
it('knows block begins', () => {
assert.deepEqual(tokenize('#+BEGIN_SRC swift'), [
{
_text: '#+BEGIN_SRC swift',
name: 'SRC',
params: ['swift'],
type: 'block.begin'
}
])
assert.deepEqual(tokenize('#+begin_src swift'), [
{
_text: '#+begin_src swift',
name: 'src',
params: ['swift'],
type: 'block.begin'
}
])
assert.deepEqual(tokenize('#+begin_example'), [
{
_text: '#+begin_example',
name: 'example',
params: [],
type: 'block.begin'
}
])
assert.deepEqual(tokenize('#+begin_ex😀mple'), [
{
_text: '#+begin_ex😀mple',
name: 'ex😀mple',
params: [],
type: 'block.begin'
}
])
assert.deepEqual(tokenize('#+begin_src swift :tangle code.swift'), [
{
_text: '#+begin_src swift :tangle code.swift',
name: 'src',
params: ['swift', ':tangle', 'code.swift'],
type: 'block.begin'
}
])
})
it('knows these are not block begins', () => {
assert.deepEqual(tokenize('#+begi😀n_src swift'), [
{
_text: '#+begi😀n_src swift',
type: 'text',
value: '#+begi😀n_src swift'
}
])
})
it('knows block ends', () => {
assert.deepEqual(tokenize('#+END_SRC'), [
{
_text: '#+END_SRC',
name: 'SRC',
type: 'block.end'
}
])
assert.deepEqual(tokenize(' #+END_SRC'), [
{
_text: '#+END_SRC',
name: 'SRC',
type: 'block.end'
}
])
assert.deepEqual(tokenize('#+end_src'), [
{
_text: '#+end_src',
name: 'src',
type: 'block.end'
}
])
assert.deepEqual(tokenize('#+end_SRC'), [
{
_text: '#+end_SRC',
name: 'SRC',
type: 'block.end'
}
])
assert.deepEqual(tokenize('#+end_S😀RC'), [
{
_text: '#+end_S😀RC',
name: 'S😀RC',
type: 'block.end'
}
])
assert.deepEqual(tokenize('#+end_SRC '), [
{
_text: '#+end_SRC ',
name: 'SRC',
type: 'block.end'
}
])
})
it('knows these are not block ends', () => {
assert.deepEqual(tokenize('#+end_src param'), [
{
_text: '#+end_src param',
type: 'text',
value: '#+end_src param'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/block.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default (reader: Reader): Token[] | undefined => {
const { match, eat, endOfLine, jump } = reader
const ws = eat('whitespaces')
const b = match(/#\+begin_([^\s\n]+)\s*(.*)$/imy, { end: endOfLine() })
if (b) {
eat('line')
const params = b.result[2]
.split(' ')
.map((p) => p.trim())
.filter(String)
return [
{
type: 'block.begin',
name: b.result[1],
params,
position: { ...b.position }
}
]
}
const e = match(/#\+end_([^\s\n]+)\s*$/imy, { end: endOfLine() })
if (e) {
reader.eat('line')
return [
{
type: 'block.end',
name: e.result[1],
position: { ...e.position }
}
]
}
ws && jump(ws.position.start)
}
================================================
FILE: packages/orga/src/tokenize/comment.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize comment', () => {
it('knows comments', () => {
assert.deepEqual(tokenize('# a comment'), [
{
_text: '# a comment',
type: 'comment',
value: 'a comment'
}
])
assert.deepEqual(tokenize('# '), [
{
_text: '# ',
type: 'comment',
value: ''
}
])
assert.deepEqual(tokenize('# a comment😯'), [
{
_text: '# a comment😯',
type: 'comment',
value: 'a comment😯'
}
])
assert.deepEqual(tokenize(' # a comment'), [
{
_text: '# a comment',
type: 'comment',
value: 'a comment'
}
])
assert.deepEqual(tokenize(' \t # a comment'), [
{
_text: '# a comment',
type: 'comment',
value: 'a comment'
}
])
assert.deepEqual(tokenize('# a comment'), [
{
_text: '# a comment',
type: 'comment',
value: 'a comment'
}
])
assert.deepEqual(tokenize('# \t a comment'), [
{
_text: '# a comment',
type: 'comment',
value: 'a comment'
}
])
})
it('knows these are not comments', () => {
assert.deepEqual(tokenize('#not a comment'), [
{
_text: '#not a comment',
type: 'text',
value: '#not a comment'
}
])
assert.deepEqual(tokenize(' #not a comment'), [
{
_text: ' #not a comment',
type: 'text',
value: ' #not a comment'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/comment.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default ({ match, eat, jump }: Reader): Token | undefined => {
const ws = eat('whitespaces')
if (match(/^#\s/y)) {
const comment = match(/^#\s+(.*)$/my)
if (comment) {
eat('line')
return {
type: 'comment',
position: comment.position,
value: comment.result[1]
}
}
}
ws && jump(ws.position.start)
}
================================================
FILE: packages/orga/src/tokenize/drawer.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize drawer', () => {
it('knows drawer begins', () => {
assert.deepEqual(tokenize(':PROPERTIES:'), [
{
_text: ':PROPERTIES:',
name: 'PROPERTIES',
type: 'drawer.begin'
}
])
assert.deepEqual(tokenize(' :properties:'), [
{
_text: ':properties:',
name: 'properties',
type: 'drawer.begin'
}
])
assert.deepEqual(tokenize(' :properties: '), [
{
_text: ':properties:',
name: 'properties',
type: 'drawer.begin'
}
])
assert.deepEqual(tokenize(' :prop_erties: '), [
{
_text: ':prop_erties:',
name: 'prop_erties',
type: 'drawer.begin'
}
])
})
it('knows these are not drawer begins', () => {
assert.deepEqual(tokenize('PROPERTIES:'), [
{
_text: 'PROPERTIES:',
type: 'text',
value: 'PROPERTIES:'
}
])
assert.deepEqual(tokenize(':PROPERTIES'), [
{
_text: ':PROPERTIES',
type: 'text',
value: ':PROPERTIES'
}
])
assert.deepEqual(tokenize(':PR OPERTIES:'), [
{
_text: ':PR OPERTIES:',
type: 'text',
value: ':PR OPERTIES:'
}
])
})
it('knows drawer ends', () => {
assert.deepEqual(tokenize(':END:'), [
{
_text: ':END:',
type: 'drawer.end'
}
])
assert.deepEqual(tokenize(' :end:'), [
{
_text: ':end:',
type: 'drawer.end'
}
])
assert.deepEqual(tokenize(' :end: '), [
{
_text: ':end:',
type: 'drawer.end'
}
])
assert.deepEqual(tokenize(' :end: '), [
{
_text: ':end:',
type: 'drawer.end'
}
])
})
it('knows these are not drawer ends', () => {
assert.deepEqual(tokenize('END:'), [
{
_text: 'END:',
type: 'text',
value: 'END:'
}
])
assert.deepEqual(tokenize(':END'), [
{
_text: ':END',
type: 'text',
value: ':END'
}
])
assert.deepEqual(tokenize(':ENDed'), [
{
_text: ':ENDed',
type: 'text',
value: ':ENDed'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/drawer.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default (reader: Reader): Token[] => {
const { match, jump, eat } = reader
const ws = eat('whitespaces')
const drawerReg = /:(\w+):(?=[ \t]*$)/my
const m = match(drawerReg)
if (m) {
jump(m.position.end)
const name = m.result[1]
eat('whitespaces')
if (name.toLowerCase() === 'end') {
return [
{
type: 'drawer.end',
position: m.position
}
]
} else {
return [
{
type: 'drawer.begin',
name,
position: m.position
}
]
}
}
ws && jump(ws.position.start)
return []
}
================================================
FILE: packages/orga/src/tokenize/empty.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default function ({ isStartOfLine, getLine, eat }: Reader): Token[] {
const tokens: Token[] = []
while (isStartOfLine()) {
const l = getLine()
if (l === null || l.replace(/\s/g, '').length > 0) break
const line = eat('line')
if (!line) break
tokens.push({
type: 'emptyLine',
position: line.position
})
const nl = eat('newline')
if (!nl) break
tokens.push({
type: 'newline',
position: nl.position
})
}
return tokens
}
================================================
FILE: packages/orga/src/tokenize/footnote.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize footnote', () => {
it('knows footnotes', () => {
assert.deepEqual(tokenize('[fn:1] a footnote'), [
{
_text: '[fn:1]',
label: '1',
type: 'footnote.label'
},
{
_text: 'a footnote',
type: 'text',
value: 'a footnote'
}
])
assert.deepEqual(tokenize('[fn:word] a footnote'), [
{
_text: '[fn:word]',
label: 'word',
type: 'footnote.label'
},
{
_text: 'a footnote',
type: 'text',
value: 'a footnote'
}
])
assert.deepEqual(tokenize('[fn:word_] a footnote'), [
{
_text: '[fn:word_]',
label: 'word_',
type: 'footnote.label'
},
{
_text: 'a footnote',
type: 'text',
value: 'a footnote'
}
])
assert.deepEqual(tokenize('[fn:wor1d_] a footnote'), [
{
_text: '[fn:wor1d_]',
label: 'wor1d_',
type: 'footnote.label'
},
{
_text: 'a footnote',
type: 'text',
value: 'a footnote'
}
])
})
it('knows these are not footnotes', () => {
assert.deepEqual(tokenize('[fn:1]: not a footnote'), [
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '1',
label: '1',
type: 'footnote.label'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ': not a footnote',
type: 'text',
value: ': not a footnote'
}
])
assert.deepEqual(tokenize(' [fn:1] not a footnote with space prefix'), [
{
_text: ' ',
type: 'text',
value: ' '
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '1',
label: '1',
type: 'footnote.label'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' not a footnote with space prefix',
type: 'text',
value: ' not a footnote with space prefix'
}
])
assert.deepEqual(tokenize('[[fn:1] not a footnote with extra ['), [
{
_text: '[',
type: 'text',
value: '['
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '1',
label: '1',
type: 'footnote.label'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' not a footnote with extra [',
type: 'text',
value: ' not a footnote with extra ['
}
])
assert.deepEqual(tokenize('\t[fn:1] not a footnote with a tab prefix'), [
{
_text: '\t',
type: 'text',
value: '\t'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '1',
label: '1',
type: 'footnote.label'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' not a footnote with a tab prefix',
type: 'text',
value: ' not a footnote with a tab prefix'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/footnote.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
import { tokenize as tokenizeInline } from './inline/index.js'
export default (reader: Reader): Token[] | undefined => {
const { isStartOfLine, match, jump, eat } = reader
if (!isStartOfLine()) return
let tokens: Token[] = []
const m = match(/^\[fn:([^\]]+)\](?=\s)/y)
if (!m) return []
tokens.push({
type: 'footnote.label',
label: m.result[1],
position: m.position
})
jump(m.position.end)
eat('whitespaces')
tokens = tokens.concat(tokenizeInline(reader))
return tokens
}
================================================
FILE: packages/orga/src/tokenize/headline.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize headline', () => {
it('knows headlines', () => {
assert.deepEqual(tokenize('** a headline'), [
{ _text: '**', level: 2, type: 'stars' },
{ _text: 'a headline', type: 'text', value: 'a headline' }
])
assert.deepEqual(tokenize('** _headline_'), [
{ _text: '**', level: 2, type: 'stars' },
{
_text: '_headline_',
style: 'underline',
type: 'text',
value: 'headline'
}
])
assert.deepEqual(tokenize('** a headline'), [
{ _text: '**', level: 2, type: 'stars' },
{ _text: 'a headline', type: 'text', value: 'a headline' }
])
assert.deepEqual(tokenize('***** a headline'), [
{ _text: '*****', level: 5, type: 'stars' },
{ _text: 'a headline', type: 'text', value: 'a headline' }
])
assert.deepEqual(tokenize('* a 😀line'), [
{ _text: '*', level: 1, type: 'stars' },
{ _text: 'a 😀line', type: 'text', value: 'a 😀line' }
])
assert.deepEqual(tokenize('* TODO [#A] a headline :tag1:tag2:'), [
{ _text: '*', level: 1, type: 'stars' },
{
_text: 'TODO',
actionable: true,
keyword: 'TODO',
type: 'todo'
},
{ _text: '[#A]', type: 'priority', value: '[#A]' },
{ _text: 'a headline', type: 'text', value: 'a headline' },
{
_text: ':tag1:tag2:',
tags: ['tag1', 'tag2'],
type: 'tags'
}
])
assert.deepEqual(
tokenize(
'* TODO [#A] a headline :tag1:123:#hash:@at:org-mode:under_score:98%:'
),
[
{ _text: '*', level: 1, type: 'stars' },
{
_text: 'TODO',
actionable: true,
keyword: 'TODO',
type: 'todo'
},
{ _text: '[#A]', type: 'priority', value: '[#A]' },
{ _text: 'a headline', type: 'text', value: 'a headline' },
{
_text: ':tag1:123:#hash:@at:org-mode:under_score:98%:',
tags: [
'tag1',
'123',
'#hash',
'@at',
'org-mode',
'under_score',
'98%'
],
type: 'tags'
}
]
)
})
it('knows these are not headlines', () => {
assert.deepEqual(tokenize('*not a headline'), [
{ _text: '*not a headline', type: 'text', value: '*not a headline' }
])
assert.deepEqual(tokenize(' * not a headline'), [
{ _text: ' * not a headline', type: 'text', value: ' * not a headline' }
])
assert.deepEqual(tokenize('*_* not a headline'), [
{ _text: '*_*', style: 'bold', type: 'text', value: '_' },
{ _text: ' not a headline', type: 'text', value: ' not a headline' }
])
assert.deepEqual(tokenize('not a headline'), [
{ _text: 'not a headline', type: 'text', value: 'not a headline' }
])
})
})
================================================
FILE: packages/orga/src/tokenize/headline.ts
================================================
import type { Reader } from 'text-kit'
import type { TodoKeywordSet } from '../todo.js'
import type { Token } from '../types.js'
import { tokenize } from './inline/index.js'
export default (todo: TodoKeywordSet) =>
(reader: Reader): Token[] | undefined => {
const { isStartOfLine, match, now, eol, eat, jump, substring, endOfLine } =
reader
if (!isStartOfLine() || !match(/^\*+[ \t]+/my)) return
// TODO: cache this, for performance sake
const todos = todo.keywords
let buffer: Token[] = []
const stars = eat(/^\*+(?=[ \t])/)
if (!stars) throw Error('not gonna happen')
buffer.push({
type: 'stars',
level: stars.value.length,
position: {
start: stars.position.start,
end: eat('whitespaces').position.start
}
})
const keyword = eat(
RegExp(`${todos.map(encodeURIComponent).join('|')}(?=[ \t])`, 'y')
)
if (keyword) {
buffer.push({
type: 'todo',
keyword: keyword.value,
actionable: todo.actionable(keyword.value),
position: {
start: keyword.position.start,
end: eat('whitespaces').position.start
}
})
}
const priority = eat(/^\[#(A|B|C)\](?=[ \t])/y)
if (priority) {
buffer.push({
type: 'priority',
...priority,
position: {
start: priority.position.start,
end: eat('whitespaces').position.start
}
})
}
const tags = match(/[ \t]+(:(?:[\w@_#%-]+:)+)[ \t]*$/m, {
end: endOfLine()
})
let contentEnd = eol(now().line)
if (tags) {
contentEnd = tags.position.start
}
const r = reader.read({ end: contentEnd })
const tokens = tokenize(r)
jump(r.now())
buffer = buffer.concat(tokens)
if (tags) {
eat('whitespaces')
const tagsPosition = { start: now(), end: tags.position.end }
const s = substring(tagsPosition.start, tagsPosition.end)
buffer.push({
type: 'tags',
tags: s
.split(':')
.map((t) => t.trim())
.filter(Boolean),
position: { start: now(), end: tags.position.end }
})
jump(tags.position.end)
}
return buffer
}
================================================
FILE: packages/orga/src/tokenize/hr.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize hr', () => {
it('knows horizontal rules', () => {
assert.deepEqual(tokenize('-----'), [
{
_text: '-----',
type: 'hr'
}
])
assert.deepEqual(tokenize('------'), [
{
_text: '------',
type: 'hr'
}
])
assert.deepEqual(tokenize('--------'), [
{
_text: '--------',
type: 'hr'
}
])
assert.deepEqual(tokenize(' -----'), [
{
_text: ' -----',
type: 'hr'
}
])
assert.deepEqual(tokenize('----- '), [
{
_text: '----- ',
type: 'hr'
}
])
assert.deepEqual(tokenize(' ----- '), [
{
_text: ' ----- ',
type: 'hr'
}
])
assert.deepEqual(tokenize(' ----- \t '), [
{
_text: ' ----- ',
type: 'hr'
}
])
})
it('knows these are not horizontal rules', () => {
assert.deepEqual(tokenize('----'), [
{
_text: '----',
type: 'text',
value: '----'
}
])
assert.deepEqual(tokenize('- ----'), [
{
_text: '-',
indent: 0,
ordered: false,
type: 'list.item.bullet'
},
{
_text: '----',
type: 'text',
value: '----'
}
])
assert.deepEqual(tokenize('-----a'), [
{
_text: '-----a',
type: 'text',
value: '-----a'
}
])
assert.deepEqual(tokenize('_-----'), [
{
_text: '_-----',
type: 'text',
value: '_-----'
}
])
assert.deepEqual(tokenize('----- a'), [
{
_text: '----- a',
type: 'text',
value: '----- a'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/hr.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default ({ eat }: Reader): Token | undefined => {
const hr = eat(/^\s*-{5,}\s*$/my)
if (hr) {
return {
type: 'hr',
position: hr.position
}
}
}
================================================
FILE: packages/orga/src/tokenize/index.ts
================================================
import { type Reader, read } from 'text-kit'
import type { Point, Position } from 'unist'
import type { LexerOptions } from '../options.js'
import type { TodoManager } from '../todo.js'
import type { Token } from '../types.js'
import block from './block.js'
import comment from './comment.js'
import drawer from './drawer.js'
import emptyLines from './empty.js'
import footnote from './footnote.js'
import headline from './headline.js'
import hr from './hr.js'
import { tokenize as inlineTok } from './inline/index.js'
import keyword from './keyword.js'
import latex from './latex.js'
import listItem from './list.js'
import planning from './planning.js'
import table from './table.js'
const PLANNING_KEYWORDS = ['DEADLINE', 'SCHEDULED', 'CLOSED']
export interface Lexer {
eat: (type?: string) => Token | undefined
eatAll: (type: string) => number
peek: (offset?: number) => Token | undefined
match: (cond: RegExp | string, offset?: number) => boolean
all: () => Token[]
save: () => number
restore: (point: number) => void
substring: (position: Position) => string
/** Modify the next token (or the token at the given offset). */
modify(f: (t: Token) => Token, offset?: number): void
readonly now: number
toOffset: (point: Point | number) => number
toPoint: (point: number) => Point
readonly todo: TodoManager
}
export type Tokenizer = (reader: Reader) => Token[] | Token | undefined
export const tokenize = (text: string, options: LexerOptions): Lexer => {
const { timezone, range, todo } = options
const reader = read(text, range)
const { getChar } = reader
let tokens: Token[] = []
let cursor = 0
const tokenizers: Tokenizer[] = [
({ getChar, eat }) =>
getChar() === '\n' && {
type: 'newline',
position: eat('char').position
},
headline(todo),
drawer,
planning({ keywords: PLANNING_KEYWORDS, timezone }),
keyword,
block,
latex,
listItem,
comment,
table,
hr,
footnote
]
function tok(): Token[] {
const all = emptyLines(reader)
// eat('whitespaces')
if (!getChar()) return all
for (const t of tokenizers) {
const result = t(reader)
if (!result) continue
const tokens = Array.isArray(result) ? result : [result]
if (tokens.length > 0) {
return [...all, ...tokens]
}
}
// last resort
const currentLine = reader.read({ end: reader.endOfLine() })
const inlineTokens = inlineTok(currentLine)
reader.jump(currentLine.now())
return [...all, ...inlineTokens]
}
const peek = (offset = 0): Token | undefined => {
const pos = cursor + offset
if (pos >= tokens.length) {
tokens = tokens.concat(tok())
}
return tokens[pos]
}
const modify = (f: (t: Token) => Token, offset = 0): void => {
const pos = cursor + offset
const token = peek(offset)
if (token !== undefined) {
tokens[pos] = f(token)
}
}
const _eat = (type: string | undefined = undefined): Token | undefined => {
const t = peek()
if (!t) return undefined
if (!type || type === t.type) {
cursor += 1
return t
}
return undefined
}
return {
peek,
eat: _eat,
eatAll(type: string): number {
let count = 0
while (_eat(type)) {
count += 1
}
return count
},
match(cond, _offset = 0) {
const token = peek()
if (!token) return false
if (typeof cond === 'string') {
return token.type === cond
}
return cond.test(token.type)
},
all(_max: number | undefined = undefined): Token[] {
let _all: Token[] = []
let tokens = tok()
while (tokens.length > 0) {
_all = _all.concat(tokens)
tokens = tok()
}
return _all
},
save: () => cursor,
restore(point) {
cursor = point
},
substring: (pos) => reader.substring(pos.start, pos.end),
modify,
get now() {
const token = peek()
return reader.toIndex(token?.position.start ?? Infinity)
},
toOffset: (point) => reader.toIndex(point),
toPoint: (offset) => reader.toPoint(offset),
get todo() {
return todo
}
}
}
================================================
FILE: packages/orga/src/tokenize/inline/footnote.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../../types.js'
import { tokenize } from './index.js'
const tokFootnoteRefernece = (reader: Reader) => {
const tokens: Token[] = []
const { eat, now, jump } = reader
const fnb = eat(/^\[fn:/)
if (!fnb) return
tokens.push({
type: 'opening',
element: 'footnote.reference',
position: fnb.position
})
const closing = reader.findClosing(fnb.position.start)
if (!closing) return
const label = eat(/^[\w_-]+/)
if (label) {
tokens.push({
type: 'footnote.label',
label: label.value,
position: label.position
})
}
if (label && now().offset === closing.offset) {
tokens.push({
type: 'closing',
element: 'footnote.reference',
position: eat().position
})
return tokens
}
if (!eat(/^:/)) return
const defRange = {
start: now(),
end: closing
}
const more = tokenize(reader.read(defRange))
tokens.push(...more)
jump(closing)
tokens.push({
type: 'closing',
element: 'footnote.reference',
position: eat().position
})
return tokens
}
export default tokFootnoteRefernece
================================================
FILE: packages/orga/src/tokenize/inline/index.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../../types.js'
import type { Tokenizer } from '../index.js'
import tokenizeFootnoteRef from './footnote.js'
import tokenizeLink from './link.js'
import tokenizeMath from './math.js'
import tokenizeText from './text.js'
const ALL: Tokenizer[] = [
tokenizeFootnoteRef,
tokenizeLink,
tokenizeMath,
tokenizeText()
]
export const tokenize = (
reader: Reader,
tokenizers: Tokenizer[] = ALL,
{ ignoring }: { ignoring: string[] } = { ignoring: [] }
): Token[] => {
const { now, eat, jump, substring, getChar, toPoint } = reader
const _tokens: Token[] = []
let cursor = now().offset
const push = (...tokens: Token[]) => {
if (tokens.length === 0) return
// collect plain text
const textEnd = tokens[0].position.start
if (cursor < textEnd.offset) {
_tokens.push({
type: 'text',
value: substring(cursor, textEnd),
position: { start: toPoint(cursor), end: { ...textEnd } }
})
}
cursor = tokens[tokens.length - 1].position.end.offset
_tokens.push(...tokens)
}
main: while (getChar()) {
const newline = eat('newline')
if (newline) {
push({
type: 'newline',
position: newline.position
})
break // newline breaks inline
}
for (const t of tokenizers) {
const r = reader.read()
const tokens = t(r)
if (tokens) {
push(...(Array.isArray(tokens) ? tokens : [tokens]))
jump(r.now())
continue main
}
}
eat()
}
if (cursor < now().offset) {
const value = substring(cursor, reader.now())
_tokens.push({
type: 'text',
value,
position: { start: toPoint(cursor), end: reader.now() }
})
}
return _tokens
}
================================================
FILE: packages/orga/src/tokenize/inline/link.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../../types.js'
import uri from '../../uri.js'
import type { Tokenizer } from '../index.js'
import { tokenize } from './index.js'
import tokenizeText from './text.js'
const tokenizeLink: Tokenizer = (reader: Reader) => {
const tokens: Token[] = []
const { eat, findClosing, jump, getChar, now } = reader
if (getChar() !== '[') {
return
}
const linkOpening = eat('char')
// if (!linkOpening) return
tokens.push({
type: 'opening',
element: 'link',
position: linkOpening.position
})
const linkClosing = findClosing(linkOpening.position.start)
if (!linkClosing) return
if (getChar() !== '[') {
return
}
const pathOpening = eat('char')
const pathClosing = findClosing(pathOpening.position.start)
if (!pathClosing) return
const linkInfo = uri(reader.substring(pathOpening.position.end, pathClosing))
if (!linkInfo) return
jump(pathClosing)
eat('char') // eat the ]
tokens.push({
type: 'link.path',
...linkInfo,
position: {
start: pathOpening.position.start,
end: now()
}
})
if (getChar() === '[') {
const descClosing = findClosing()
if (!descClosing) {
return
}
eat() // descOpening
const desc = tokenize(reader.read({ end: descClosing }), [
tokenizeText(now())
])
tokens.push(...desc)
}
jump(linkClosing)
tokens.push({
type: 'closing',
element: 'link',
position: eat().position
})
return tokens
}
export default tokenizeLink
================================================
FILE: packages/orga/src/tokenize/inline/math.ts
================================================
import type { Token } from '../../types.js'
import type { Tokenizer } from '../index.js'
const tokenizeMath: Tokenizer = (reader) => {
const { now, eat, getChar, jump, substring, match } = reader
const tokens: Token[] = []
const tokenStart = now()
let closingMatch: RegExp | undefined
if (getChar() === '\\') {
eat()
const opening = getChar()
if (opening === '(') {
closingMatch = /\\\)/
} else if (opening === '[') {
closingMatch = /\\]/
} else return
eat()
} else if (getChar() === '$') {
eat()
if (getChar() === '$') {
eat()
closingMatch = /\$\$/
} else return
}
if (!closingMatch) return
const valueStart = now()
const m = match(closingMatch)
if (!m) return
const valueEnd = m.position.start
jump(m.position.end)
const tokenEnd = now()
tokens.push({
type: 'text',
style: 'math',
value: substring(valueStart, valueEnd),
position: { start: tokenStart, end: tokenEnd }
})
return tokens
}
export default tokenizeMath
================================================
FILE: packages/orga/src/tokenize/inline/text.ts
================================================
import type { Reader } from 'text-kit'
import type { Point } from 'unist'
import type { Style, Token } from '../../types.js'
const MARKERS: { [key: string]: Style } = {
'*': 'bold',
'=': 'verbatim',
'/': 'italic',
'+': 'strikeThrough',
_: 'underline',
'~': 'code'
}
const tokenizeText =
(bol: Point | undefined = undefined) =>
(reader: Reader) => {
const tokens: Token[] = []
const { now, eat, jump, getChar, findClosing, substring } = reader
const marker = getChar()
const style = MARKERS[marker]
if (!style) return
// check pre
const pre = getChar(-1)
const isBOL = (bol && bol.offset === now().offset) || now().column === 1
if (!isBOL && !/[\s({'"]/.test(pre)) return
const tokenStart = now()
const closing = findClosing(now())
if (!closing) return
eat()
const valueStart = now()
// check border
if (getChar().match(/\s/)) return
jump(closing)
// check border
if (getChar(-1).match(/\s/)) return
// check post
const post = getChar(1)
if (post && ` \t\n-.,;:!?')}["`.indexOf(post) === -1) return
const valueEnd = now()
eat() // closing
tokens.push({
type: 'text',
style,
value: substring(valueStart, valueEnd),
position: { start: tokenStart, end: now() }
})
return tokens
}
export default tokenizeText
================================================
FILE: packages/orga/src/tokenize/inline.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('Inline Tokenization', () => {
it('recon single emphasis', () => {
assert.deepEqual(tokenize('hello *world*, welcome to *org-mode*.'), [
{
_text: 'hello ',
type: 'text',
value: 'hello '
},
{
_text: '*world*',
style: 'bold',
type: 'text',
value: 'world'
},
{
_text: ', welcome to ',
type: 'text',
value: ', welcome to '
},
{
_text: '*org-mode*',
style: 'bold',
type: 'text',
value: 'org-mode'
},
{
_text: '.',
type: 'text',
value: '.'
}
])
})
it('recon emphasises at different locations', () => {
assert.deepEqual(tokenize('one *two* three'), [
{
_text: 'one ',
type: 'text',
value: 'one '
},
{
_text: '*two*',
style: 'bold',
type: 'text',
value: 'two'
},
{
_text: ' three',
type: 'text',
value: ' three'
}
])
assert.deepEqual(tokenize('*one* two three'), [
{
_text: '*one*',
style: 'bold',
type: 'text',
value: 'one'
},
{
_text: ' two three',
type: 'text',
value: ' two three'
}
])
assert.deepEqual(tokenize('one two *three*'), [
{
_text: 'one two ',
type: 'text',
value: 'one two '
},
{
_text: '*three*',
style: 'bold',
type: 'text',
value: 'three'
}
])
})
it('recon link', () => {
assert.deepEqual(tokenize(`hello [[./image/logo.png]]`), [
{
_text: 'hello ',
type: 'text',
value: 'hello '
},
{
_text: '[',
element: 'link',
type: 'opening'
},
{
_text: '[./image/logo.png]',
protocol: 'file',
search: undefined,
type: 'link.path',
value: './image/logo.png'
},
{
_text: ']',
element: 'link',
type: 'closing'
}
])
assert.deepEqual(tokenize(`hello [[Internal Link][link]]`), [
{
_text: 'hello ',
type: 'text',
value: 'hello '
},
{
_text: '[',
element: 'link',
type: 'opening'
},
{
_text: '[Internal Link]',
protocol: 'internal',
search: undefined,
type: 'link.path',
value: 'Internal Link'
},
{
_text: 'link',
type: 'text',
value: 'link'
},
{
_text: ']',
element: 'link',
type: 'closing'
}
])
assert.deepEqual(tokenize(`hello [[../image/logo.png][logo]]`), [
{
_text: 'hello ',
type: 'text',
value: 'hello '
},
{
_text: '[',
element: 'link',
type: 'opening'
},
{
_text: '[../image/logo.png]',
protocol: 'file',
search: undefined,
type: 'link.path',
value: '../image/logo.png'
},
{
_text: 'logo',
type: 'text',
value: 'logo'
},
{
_text: ']',
element: 'link',
type: 'closing'
}
])
assert.deepEqual(tokenize(`that is a [[../image/logo.png][/nice/ logo]]`), [
{
_text: 'that is a ',
type: 'text',
value: 'that is a '
},
{
_text: '[',
element: 'link',
type: 'opening'
},
{
_text: '[../image/logo.png]',
protocol: 'file',
search: undefined,
type: 'link.path',
value: '../image/logo.png'
},
{
_text: '/nice/',
style: 'italic',
type: 'text',
value: 'nice'
},
{
_text: ' logo',
type: 'text',
value: ' logo'
},
{
_text: ']',
element: 'link',
type: 'closing'
}
])
})
it('recon footnote reference', () => {
assert.deepEqual(tokenize(`hello[fn:1] world.`), [
{
_text: 'hello',
type: 'text',
value: 'hello'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '1',
label: '1',
type: 'footnote.label'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' world.',
type: 'text',
value: ' world.'
}
])
})
it('recon anonymous footnote reference', () => {
assert.deepEqual(tokenize('hello[fn::Anonymous] world.'), [
{
_text: 'hello',
type: 'text',
value: 'hello'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: 'Anonymous',
type: 'text',
value: 'Anonymous'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' world.',
type: 'text',
value: ' world.'
}
])
})
it('recon anonymous footnote reference with inner footnote reference', () => {
assert.deepEqual(tokenize('hello[fn::[fn::Anonymous]] world.'), [
{
_text: 'hello',
type: 'text',
value: 'hello'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: 'Anonymous',
type: 'text',
value: 'Anonymous'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' world.',
type: 'text',
value: ' world.'
}
])
})
it('recon anonymous footnote reference with empty body', () => {
assert.deepEqual(tokenize('hello[fn::] world.'), [
{
_text: 'hello',
type: 'text',
value: 'hello'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' world.',
type: 'text',
value: ' world.'
}
])
})
it('recon named inline footnote', () => {
assert.deepEqual(tokenize('hello[fn:named:Inline named footnote] world.'), [
{
_text: 'hello',
type: 'text',
value: 'hello'
},
{
_text: '[fn:',
element: 'footnote.reference',
type: 'opening'
},
{
_text: 'named',
label: 'named',
type: 'footnote.label'
},
{
_text: 'Inline named footnote',
type: 'text',
value: 'Inline named footnote'
},
{
_text: ']',
element: 'footnote.reference',
type: 'closing'
},
{
_text: ' world.',
type: 'text',
value: ' world.'
}
])
})
it('recon invalid inline markups', () => {
assert.deepEqual(tokenize(`* word*`), [
{
_text: '*',
level: 1,
type: 'stars'
},
{
_text: 'word*',
type: 'text',
value: 'word*'
}
])
assert.deepEqual(tokenize(`*word *`), [
{
_text: '*word *',
type: 'text',
value: '*word *'
}
])
})
it('recon emphasises with 2 chars', () => {
assert.deepEqual(tokenize(`*12*`), [
{
_text: '*12*',
style: 'bold',
type: 'text',
value: '12'
}
])
assert.deepEqual(tokenize(`*1*`), [
{
_text: '*1*',
style: 'bold',
type: 'text',
value: '1'
}
])
})
it('recon mixed emphasis', () => {
assert.deepEqual(
tokenize(
"[[https://github.com/xiaoxinghu/orgajs][Here's]] to the *crazy* ones, the /misfits/, the _rebels_, the ~troublemakers~, the round pegs in the +round+ square holes..."
),
[
{
_text: '[',
element: 'link',
type: 'opening'
},
{
_text: '[https://github.com/xiaoxinghu/orgajs]',
protocol: 'https',
search: undefined,
type: 'link.path',
value: 'https://github.com/xiaoxinghu/orgajs'
},
{
_text: "Here's",
type: 'text',
value: "Here's"
},
{
_text: ']',
element: 'link',
type: 'closing'
},
{
_text: ' to the ',
type: 'text',
value: ' to the '
},
{
_text: '*crazy*',
style: 'bold',
type: 'text',
value: 'crazy'
},
{
_text: ' ones, the ',
type: 'text',
value: ' ones, the '
},
{
_text: '/misfits/',
style: 'italic',
type: 'text',
value: 'misfits'
},
{
_text: ', the ',
type: 'text',
value: ', the '
},
{
_text: '_rebels_',
style: 'underline',
type: 'text',
value: 'rebels'
},
{
_text: ', the ',
type: 'text',
value: ', the '
},
{
_text: '~troublemakers~',
style: 'code',
type: 'text',
value: 'troublemakers'
},
{
_text: ', the round pegs in the ',
type: 'text',
value: ', the round pegs in the '
},
{
_text: '+round+',
style: 'strikeThrough',
type: 'text',
value: 'round'
},
{
_text: ' square holes...',
type: 'text',
value: ' square holes...'
}
]
)
})
it('can handle something more complicated', () => {
const content = `
Special characters =~= and =!=. Also =~/.this/path= and ~that~ thing.
`
assert.deepEqual(tokenize(content), [
{
_text: '',
type: 'emptyLine'
},
{
_text: '\n',
type: 'newline'
},
{
_text: 'Special characters ',
type: 'text',
value: 'Special characters '
},
{
_text: '=~=',
style: 'verbatim',
type: 'text',
value: '~'
},
{
_text: ' and ',
type: 'text',
value: ' and '
},
{
_text: '=!=',
style: 'verbatim',
type: 'text',
value: '!'
},
{
_text: '. Also ',
type: 'text',
value: '. Also '
},
{
_text: '=~/.this/path=',
style: 'verbatim',
type: 'text',
value: '~/.this/path'
},
{
_text: ' and ',
type: 'text',
value: ' and '
},
{
_text: '~that~',
style: 'code',
type: 'text',
value: 'that'
},
{
_text: ' thing.',
type: 'text',
value: ' thing.'
},
{
_text: '\n',
type: 'newline'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/keyword.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default (reader: Reader): Token[] | undefined => {
const keyword = reader.match(/^#\+(\w+):(?:[ \t]+(.*))?$/my)
if (keyword) {
reader.eat('line')
const tokens: Token[] = [
{
type: 'keyword',
key: keyword.result[1],
value: keyword.result[2] ?? '',
position: keyword.position
}
]
const nl = reader.eat('newline')
if (nl) {
tokens.push({
type: 'newline',
position: nl.position
})
}
return tokens
}
}
================================================
FILE: packages/orga/src/tokenize/keywords.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize keywords', () => {
it('knows keywords', () => {
assert.deepEqual(tokenize('#+KEY: Value'), [
{
_text: '#+KEY: Value',
key: 'KEY',
type: 'keyword',
value: 'Value'
}
])
assert.deepEqual(tokenize('#+KEY: Another Value'), [
{
_text: '#+KEY: Another Value',
key: 'KEY',
type: 'keyword',
value: 'Another Value'
}
])
assert.deepEqual(tokenize('#+KEY: value : Value'), [
{
_text: '#+KEY: value : Value',
key: 'KEY',
type: 'keyword',
value: 'value : Value'
}
])
})
it('knows these are not keywords', () => {
assert.deepEqual(tokenize('#+KEY : Value'), [
{
_text: '#+KEY : Value',
type: 'text',
value: '#+KEY : Value'
}
])
assert.deepEqual(tokenize('#+KE Y: Value'), [
{
_text: '#+KE Y: Value',
type: 'text',
value: '#+KE Y: Value'
}
])
})
it('ignores empty keywords', () => {
assert.deepEqual(tokenize('#+todo:'), [
{
_text: '#+todo:',
key: 'todo',
type: 'keyword',
value: ''
}
])
assert.deepEqual(tokenize('#+todo: '), [
{
_text: '#+todo: ',
key: 'todo',
type: 'keyword',
value: ''
}
])
assert.deepEqual(tokenize('#+todo:\ncontent'), [
{
_text: '#+todo:',
key: 'todo',
type: 'keyword',
value: ''
},
{
_text: '\n',
type: 'newline'
},
{
_text: 'content',
type: 'text',
value: 'content'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/latex.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
export default (reader: Reader): Token[] | undefined => {
const { match, eat, endOfLine } = reader
const b = match(/\\begin\{([a-zA-Z0-9*]+)\}\s*$/imy, { end: endOfLine() })
if (b) {
eat('line')
return [
{
type: 'latex.begin',
name: b.result[1],
position: { ...b.position }
}
]
}
const e = match(/\\end\{([a-zA-Z0-9*]+)\}\s*$/imy, { end: endOfLine() })
if (e) {
reader.eat('line')
return [
{
type: 'latex.end',
name: e.result[1],
position: { ...e.position }
}
]
}
}
================================================
FILE: packages/orga/src/tokenize/list.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize list item', () => {
it('knows list items', () => {
// unordered
assert.deepEqual(tokenize('- buy milk'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: 'buy milk', type: 'text', value: 'buy milk' }
])
assert.deepEqual(tokenize('+ buy milk'), [
{ _text: '+', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: 'buy milk', type: 'text', value: 'buy milk' }
])
// ordered
assert.deepEqual(tokenize('1. buy milk'), [
{ _text: '1.', indent: 0, ordered: true, type: 'list.item.bullet' },
{ _text: 'buy milk', type: 'text', value: 'buy milk' }
])
assert.deepEqual(tokenize('12. buy milk'), [
{
_text: '12.',
indent: 0,
ordered: true,
type: 'list.item.bullet'
},
{
_text: 'buy milk',
type: 'text',
value: 'buy milk'
}
])
assert.deepEqual(tokenize('123) buy milk'), [
{
_text: '123)',
indent: 0,
ordered: true,
type: 'list.item.bullet'
},
{
_text: 'buy milk',
type: 'text',
value: 'buy milk'
}
])
// checkbox
assert.deepEqual(tokenize('- [x] buy milk checked'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: '[x]', checked: true, type: 'list.item.checkbox' },
{ _text: 'buy milk checked', type: 'text', value: 'buy milk checked' }
])
assert.deepEqual(tokenize('- [X] buy milk checked'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: '[X]', checked: true, type: 'list.item.checkbox' },
{ _text: 'buy milk checked', type: 'text', value: 'buy milk checked' }
])
assert.deepEqual(tokenize('- [-] buy milk checked'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: '[-]', checked: true, type: 'list.item.checkbox' },
{ _text: 'buy milk checked', type: 'text', value: 'buy milk checked' }
])
assert.deepEqual(tokenize('- [ ] buy milk unchecked'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: '[ ]', checked: false, type: 'list.item.checkbox' },
{
_text: 'buy milk unchecked',
type: 'text',
value: 'buy milk unchecked'
}
])
// indent
assert.deepEqual(tokenize(' - buy milk'), [
{ _text: '-', indent: 2, ordered: false, type: 'list.item.bullet' },
{ _text: 'buy milk', type: 'text', value: 'buy milk' }
])
// tag
assert.deepEqual(tokenize('- item1 :: description here'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: 'item1', type: 'list.item.tag', value: 'item1' },
{
_text: 'description here',
type: 'text',
value: 'description here'
}
])
assert.deepEqual(tokenize('- item2\n :: description here'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: 'item2', type: 'text', value: 'item2' },
{ _text: '\n', type: 'newline' },
{
_text: ' :: description here',
type: 'text',
value: ' :: description here'
}
])
assert.deepEqual(tokenize('- [x] item3 :: description here'), [
{ _text: '-', indent: 0, ordered: false, type: 'list.item.bullet' },
{ _text: '[x]', checked: true, type: 'list.item.checkbox' },
{ _text: 'item3', type: 'list.item.tag', value: 'item3' },
{
_text: 'description here',
type: 'text',
value: 'description here'
}
])
})
it('knows these are not list items', () => {
assert.deepEqual(tokenize('-not item'), [
{ _text: '-not item', type: 'text', value: '-not item' }
])
assert.deepEqual(tokenize('1.not item'), [
{ _text: '1.not item', type: 'text', value: '1.not item' }
])
assert.deepEqual(tokenize('8)not item'), [
{ _text: '8)not item', type: 'text', value: '8)not item' }
])
assert.deepEqual(tokenize('8a) not item'), [
{ _text: '8a) not item', type: 'text', value: '8a) not item' }
])
})
})
================================================
FILE: packages/orga/src/tokenize/list.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
import { tokenize as tokenizeInline } from './inline/index.js'
export default (reader: Reader): Token[] => {
const { now, match, eat, jump, substring, endOfLine } = reader
const ws = eat('whitespaces')
let tokens: Token[] = []
const indent = now().column - 1
const bullet = match(/^([-+]|\d+[.)])(?=\s)/y)
if (!bullet) {
ws && jump(ws.position.start)
return []
}
tokens.push({
type: 'list.item.bullet',
indent,
ordered: /^\d/.test(bullet.result[1]),
position: bullet.position
})
jump(bullet.position.end)
eat('whitespaces')
const checkbox = match(/^\[(x|X|-| )\](?=\s)/y)
if (checkbox) {
tokens.push({
type: 'list.item.checkbox',
checked: checkbox.result[1] !== ' ',
position: checkbox.position
})
jump(checkbox.position.end)
}
eat('whitespaces')
const tagMark = match(/\s+::\s+/, { end: endOfLine() })
if (tagMark) {
const pos = { start: now(), end: tagMark.position.start }
tokens.push({
type: 'list.item.tag',
value: substring(pos.start, pos.end),
position: pos
})
jump(tagMark.position.end)
}
tokens = tokens.concat(tokenizeInline(reader))
return tokens
}
================================================
FILE: packages/orga/src/tokenize/partial.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('partial tokenize', () => {
it('can start from the middle', () => {
assert.deepEqual(tokenize('a b c', { range: { start: 2 } }), [
{ type: 'text', value: 'b c', _text: 'b c' }
])
})
})
================================================
FILE: packages/orga/src/tokenize/planning.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
const options = {
timezone: 'Pacific/Auckland'
}
describe('tokenize planning', () => {
it('knows plannings', () => {
assert.deepEqual(tokenize('DEADLINE: <2018-01-01 Mon>', options), [
{
_text: 'DEADLINE:',
type: 'planning.keyword',
value: 'DEADLINE'
},
{
_text: ' <2018-01-01 Mon>',
type: 'planning.timestamp',
value: {
date: new Date('2017-12-31T11:00:00.000Z'),
end: undefined
}
}
])
assert.deepEqual(tokenize(' DEADLINE: <2018-01-01 Mon>', options), [
{
_text: 'DEADLINE:',
type: 'planning.keyword',
value: 'DEADLINE'
},
{
_text: ' <2018-01-01 Mon>',
type: 'planning.timestamp',
value: {
date: new Date('2017-12-31T11:00:00.000Z'),
end: undefined
}
}
])
assert.deepEqual(tokenize(' \tDEADLINE: <2018-01-01 Mon>', options), [
{
_text: 'DEADLINE:',
type: 'planning.keyword',
value: 'DEADLINE'
},
{
_text: ' <2018-01-01 Mon>',
type: 'planning.timestamp',
value: {
date: new Date('2017-12-31T11:00:00.000Z'),
end: undefined
}
}
])
assert.deepEqual(tokenize(' \t DEADLINE: <2018-01-01 Mon>', options), [
{
_text: 'DEADLINE:',
type: 'planning.keyword',
value: 'DEADLINE'
},
{
_text: ' <2018-01-01 Mon>',
type: 'planning.timestamp',
value: {
date: new Date('2017-12-31T11:00:00.000Z'),
end: undefined
}
}
])
})
it('know multiple plannings', () => {
assert.deepEqual(
tokenize(
'DEADLINE: <2020-07-03 Fri> SCHEDULED: <2020-07-03 Fri>',
options
),
[
{
_text: 'DEADLINE:',
type: 'planning.keyword',
value: 'DEADLINE'
},
{
_text: ' <2020-07-03 Fri> ',
type: 'planning.timestamp',
value: {
date: new Date('2020-07-02T12:00:00.000Z'),
end: undefined
}
},
{
_text: 'SCHEDULED:',
type: 'planning.keyword',
value: 'SCHEDULED'
},
{
_text: ' <2020-07-03 Fri>',
type: 'planning.timestamp',
value: {
date: new Date('2020-07-02T12:00:00.000Z'),
end: undefined
}
}
]
)
})
it('knows these are not plannings', () => {
assert.deepEqual(tokenize('dEADLINE: <2018-01-01 Mon>', options), [
{
_text: 'dEADLINE: <2018-01-01 Mon>',
type: 'text',
value: 'dEADLINE: <2018-01-01 Mon>'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/planning.ts
================================================
import type { Reader } from 'text-kit'
import type { Point } from 'unist'
import { parse as parseTimestamp } from '../timestamp.js'
import type { Token } from '../types.js'
export default ({
keywords,
timezone
}: {
keywords: string[]
timezone: string
}) =>
(reader: Reader): Token[] | undefined => {
const { now, match, eat, substring, getLine, jump } = reader
const ws = eat('whitespaces')
const pattern = `(${keywords.join('|')}):`
if (!match(RegExp(pattern, 'y'))) {
ws && jump(ws.position.start)
return
}
const currentLine = getLine()
const { line, column, offset } = now()
const getLocation = (_offset: number): Point => ({
line,
column: column + _offset,
offset: offset + _offset
})
const all: Token[] = []
const parseLastTimestamp = (end: number) => {
if (all.length === 0) return
const { type, position } = all[all.length - 1]
if (!position) throw Error(`position is ${position}`)
if (type !== 'planning.keyword') return
const endLocation = getLocation(end)
const timestampPosition = { start: position.end, end: endLocation }
const value = substring(timestampPosition.start, timestampPosition.end)
all.push({
type: 'planning.timestamp',
value: parseTimestamp(value, { timezone }),
position: timestampPosition
})
}
const p = RegExp(pattern, 'g')
for (;;) {
const m = p.exec(currentLine)
if (m === null) break
parseLastTimestamp(m.index)
all.push({
type: 'planning.keyword',
value: m[1],
position: {
start: getLocation(m.index),
end: getLocation(p.lastIndex)
}
})
}
parseLastTimestamp(currentLine.length)
eat('line')
return all
}
================================================
FILE: packages/orga/src/tokenize/table.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import tokenize from './__tests__/tok'
describe('tokenize table', () => {
it('knows table hr', () => {
assert.deepEqual(tokenize('|----+---+----|'), [
{ _text: '|----+---+----|', type: 'table.hr' }
])
assert.deepEqual(tokenize('|--=-+---+----|'), [
{ _text: '|--=-+---+----|', type: 'table.hr' }
])
assert.deepEqual(tokenize(' |----+---+----|'), [
{ _text: '|----+---+----|', type: 'table.hr' }
])
assert.deepEqual(tokenize('|----+---+----'), [
{ _text: '|----+---+----', type: 'table.hr' }
])
assert.deepEqual(tokenize('|---'), [{ _text: '|---', type: 'table.hr' }])
assert.deepEqual(tokenize('|-'), [{ _text: '|-', type: 'table.hr' }])
})
it('knows these are not table separators', () => {
assert.deepEqual(tokenize('----+---+----|'), [
{ _text: '----+---+----|', type: 'text', value: '----+---+----|' }
])
})
it('knows table rows', () => {
assert.deepEqual(tokenize('| batman | superman | wonder woman |'), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' batman ',
type: 'text',
value: ' batman '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' superman ',
type: 'text',
value: ' superman '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' wonder woman ',
type: 'text',
value: ' wonder woman '
},
{
_text: '|',
type: 'table.columnSeparator'
}
])
assert.deepEqual(tokenize("| hello | world | y'all |"), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' hello ',
type: 'text',
value: ' hello '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' world ',
type: 'text',
value: ' world '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: " y'all ",
type: 'text',
value: " y'all "
},
{
_text: '|',
type: 'table.columnSeparator'
}
])
assert.deepEqual(tokenize(" | hello | world | y'all |"), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' hello ',
type: 'text',
value: ' hello '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' world ',
type: 'text',
value: ' world '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: " y'all ",
type: 'text',
value: " y'all "
},
{
_text: '|',
type: 'table.columnSeparator'
}
])
assert.deepEqual(tokenize("| hello | world |y'all |"), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' hello ',
type: 'text',
value: ' hello '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' world ',
type: 'text',
value: ' world '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: "y'all ",
type: 'text',
value: "y'all "
},
{
_text: '|',
type: 'table.columnSeparator'
}
])
// with empty cell
assert.deepEqual(tokenize('|| world | |'), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' world ',
type: 'text',
value: ' world '
},
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: ' ',
type: 'text',
value: ' '
},
{
_text: '|',
type: 'table.columnSeparator'
}
])
})
it('knows these are not table rows', () => {
assert.deepEqual(tokenize(" hello | world | y'all |"), [
{
_text: " hello | world | y'all |",
type: 'text',
value: " hello | world | y'all |"
}
])
assert.deepEqual(tokenize('|+'), [
{
_text: '|',
type: 'table.columnSeparator'
},
{
_text: '+',
type: 'text',
value: '+'
}
])
})
})
================================================
FILE: packages/orga/src/tokenize/table.ts
================================================
import type { Reader } from 'text-kit'
import type { Token } from '../types.js'
import { tokenize as tokenizeInline } from './inline/index.js'
export default (reader: Reader): Token[] => {
const { eat, getChar, jump, endOfLine, indexOf } = reader
const ws = eat('whitespaces')
const char = getChar()
if (char !== '|') {
ws && jump(ws.position.start)
return []
}
if (getChar(1) === '-') {
const hr = eat('line')
const tokens: Token[] = [{ type: 'table.hr', position: hr.position }]
const nl = eat('newline')
if (nl) tokens.push({ type: 'newline', position: nl.position })
return tokens
}
const startColumnSeparator: Token = {
type: 'table.columnSeparator',
position: eat().position
}
const tokens: Token[] = []
const tokCells = (): void => {
const pipe = indexOf('|')
const end = pipe || endOfLine()
if (!end) throw new Error(`what is happening: ${end}`)
const inline = tokenizeInline(reader.read({ end }))
tokens.push(...inline)
jump(end)
if (pipe) {
const c = eat('char')
tokens.push({
type: 'table.columnSeparator',
position: c.position
})
tokCells()
}
const nl = eat('newline')
if (nl) {
tokens.push({
type: 'newline',
position: nl.position
})
}
}
tokCells()
return [startColumnSeparator, ...tokens]
}
================================================
FILE: packages/orga/src/types.ts
================================================
import type {
Node,
Literal as UnistLiteral,
Parent as UnistParent
} from 'unist'
export interface Literal extends UnistLiteral {
value: string
}
export interface Parent extends UnistParent {
children: Content[]
}
export type Primitive = string | number | boolean
export interface Attributes {
[key: string]: Primitive | { [key: string]: Primitive }
}
export interface Attributed {
attributes: Attributes
}
export interface Timestamp {
date: Date
end?: Date
}
export type Properties = Record
export type Settings = Record
type PropertyValue = string | string[] | Record
export type Nodes = Document | Content | Token
// ---- Syntax Tree Nodes ----
export interface Document extends Parent {
type: 'document'
properties: Properties
children: TopLevelContent[]
}
export interface Section extends Parent {
type: 'section'
level: number
properties: Properties
children: Content[]
}
// --- content types ----
export type BlockContent =
| Section
| Paragraph
| Block
| Drawer
| Planning
| List
| Table
| HorizontalRule
| Headline
| HTML
| JSX
| Latex
type TopLevelContent = BlockContent | Keyword | Footnote
export type Content =
| TopLevelContent
| TableContent
| TableRow
| TableCell
| ListContent
| PhrasingContent
export interface Footnote extends Parent {
type: 'footnote'
label: string
}
export interface Block extends Literal, Attributed {
type: 'block'
name: string
params: string[]
children: PhrasingContent[] | BlockContent[]
}
export interface Latex extends Literal {
type: 'latex'
name: string
}
export interface Drawer extends Literal {
type: 'drawer'
name: string
}
export interface Planning extends Node {
type: 'planning'
keyword: string
timestamp: Timestamp
}
type ListContent = ListItem | List
export interface List extends Parent, Attributed {
type: 'list'
indent: number
ordered: boolean
children: ListContent[]
}
type TableContent = TableRow | TableRule
export interface Table extends Parent, Attributed {
type: 'table'
children: TableContent[]
}
export interface TableRow extends Parent {
type: 'table.row'
children: TableCell[]
}
export interface TableCell extends Parent {
type: 'table.cell'
children: PhrasingContent[]
}
export interface ListItem extends Parent {
type: 'list.item'
indent: number
tag?: string
children: PhrasingContent[]
}
export interface Headline extends Parent {
type: 'headline'
level: number
keyword?: string
actionable: boolean
priority?: string
tags?: string[]
children: PhrasingContent[]
}
export interface Paragraph extends Parent, Attributed {
type: 'paragraph'
children: PhrasingContent[]
}
export interface HTML extends Literal {
type: 'html'
}
export interface JSX extends Literal {
type: 'jsx'
}
// ---- Tokens ----
export type Token =
| Keyword
| Todo
| Newline
| EmptyLine
| HorizontalRule
| Stars
| Priority
| Tags
| PlanningKeyword
| PlanningTimestamp
| ListItemTag
| ListItemCheckbox
| ListItemBullet
| TableRule
| TableColumnSeparator
| PhrasingContent
| FootnoteLabel
| BlockBegin
| BlockEnd
| LatexBegin
| LatexEnd
| DrawerBegin
| DrawerEnd
| Comment
| Opening
| Closing
| LinkPath
export type PhrasingContent =
| Text
| Link
| FootnoteReference
| Newline
| EmptyLine
| HTML
| JSX
export interface HorizontalRule extends Node {
type: 'hr'
}
export interface Newline extends Node {
type: 'newline'
}
export interface EmptyLine extends Node {
type: 'emptyLine'
}
export type Style =
| 'bold'
| 'verbatim'
| 'italic'
| 'strikeThrough'
| 'underline'
| 'code'
| 'math'
export interface Text extends Literal {
type: 'text'
style?: Style
}
export interface Link extends Parent, Attributed {
type: 'link'
path: Omit
children: PhrasingContent[]
}
export interface LinkPath extends Literal {
type: 'link.path'
protocol: string
search?: string | number
}
export type Enclosed = Style | 'link' | 'footnote.reference'
export interface Opening extends Node {
type: 'opening'
element: Enclosed
}
export interface Closing extends Node {
type: 'closing'
element: Enclosed
}
/**
* A footnote reference, which is either:
*
* `[fn:LABEL]` - a plain footnote reference.
*
* `[fn:LABEL:DEFINITION]` - an inline footnote definition.
*
* `[fn::DEFINITION]` - an anonymous (inline) footnote definition.
*
* See https://orgmode.org/worg/dev/org-syntax.html#Footnote_References.
*
* If `label` is the empty string, then this is treated as an
* anonymous footnote.
*
* If `children` is empty, then this is considered to not define a new
* footnote (and in which case, `label` should not be the empty
* string), if `children` is non-empty, then this is an inline
* footnote definition.
*/
export interface FootnoteReference extends Parent {
type: 'footnote.reference'
label: string
children: PhrasingContent[]
}
// headline tokens
export interface Stars extends Node {
type: 'stars'
level: number
}
export interface Todo extends Node {
type: 'todo'
keyword: string
actionable: boolean
}
export interface Priority extends Literal {
type: 'priority'
}
export interface Tags extends Node {
type: 'tags'
tags: string[]
}
// block tokens
export interface BlockBegin extends Node {
type: 'block.begin'
name: string
params: string[]
}
export interface BlockEnd extends Node {
type: 'block.end'
name: string
}
// drawer tokens
export interface DrawerBegin extends Node {
type: 'drawer.begin'
name: string
}
interface DrawerEnd extends Node {
type: 'drawer.end'
}
export interface LatexBegin extends Node {
type: 'latex.begin'
name: string
}
export interface LatexEnd extends Node {
type: 'latex.end'
name: string
}
interface Comment extends Literal {
type: 'comment'
}
export interface Keyword extends Node {
type: 'keyword'
key: string
value: string
}
export interface FootnoteLabel extends Node {
type: 'footnote.label'
label: string
}
export interface PlanningKeyword extends Literal {
type: 'planning.keyword'
}
export interface PlanningTimestamp extends UnistLiteral {
type: 'planning.timestamp'
value: Timestamp
}
export interface ListItemTag extends Literal {
type: 'list.item.tag'
}
export interface ListItemCheckbox extends Node {
type: 'list.item.checkbox'
checked: boolean
}
export interface ListItemBullet extends Node {
type: 'list.item.bullet'
ordered: boolean
indent: number
}
export interface TableRule extends Node {
type: 'table.hr'
}
export interface TableColumnSeparator extends Node {
type: 'table.columnSeparator'
}
export function isSection(node: Node): node is Section {
return node.type === 'section'
}
export function isParagraph(node: Node): node is Paragraph {
return node.type === 'paragraph'
}
export function isLink(node: Node): node is Link {
return node.type === 'link'
}
export function isFootnoteReference(node: Node): node is FootnoteReference {
return node.type === 'footnote.reference'
}
export function isText(node: Node): node is Text {
return node.type === 'text'
}
declare module 'unist' {
interface Data {
/** context hash */
hash?: number | undefined
}
}
================================================
FILE: packages/orga/src/uri.test.ts
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import parse from './uri'
describe('Parsing Link', () => {
it('recon local file', () => {
assert.deepEqual(parse(`file:/hello.org`), {
protocol: 'file',
search: undefined,
value: '/hello.org'
})
assert.deepEqual(parse(`./hello.org`), {
protocol: 'file',
search: undefined,
value: './hello.org'
})
assert.deepEqual(parse(`./hello.org::23`), {
protocol: 'file',
search: 23,
value: './hello.org'
})
assert.deepEqual(parse(`./hello.org::*shopping list`), {
protocol: 'file',
search: '*shopping list',
value: './hello.org'
})
assert.deepEqual(parse(`./hello.org::apple pie`), {
protocol: 'file',
search: 'apple pie',
value: './hello.org'
})
})
it('recon other protocol', () => {
assert.deepEqual(parse(`http://google.com`), {
protocol: 'http',
search: undefined,
value: 'http://google.com'
})
assert.deepEqual(parse(`mailto:dawnstar.hu@gmail.com`), {
protocol: 'mailto',
search: undefined,
value: 'dawnstar.hu@gmail.com'
})
})
})
================================================
FILE: packages/orga/src/uri.ts
================================================
const URL_PATTERN = /(?:([a-z][a-z0-9+.-]*):)?(.*)/i
interface LinkInfo {
protocol: string
value: string
search?: string | number
}
const isFilePath = (str: string): boolean => {
return str && /^\.{0,2}\//.test(str)
}
export default (link: string): LinkInfo | undefined => {
const m = URL_PATTERN.exec(link)
if (!m) return undefined
const protocol = (
m[1] || (isFilePath(m[2]) ? `file` : `internal`)
).toLowerCase()
let value = m[2]
if (/https?/.test(protocol)) {
value = `${protocol}:${value}`
}
let search: string | number | undefined
if (protocol === 'file') {
const m = /(.*?)::(.*)/.exec(value)
if (m?.[1] && m[2]) {
value = m[1]
search = parseInt(m[2], 10)
search = Number.isInteger(search) ? search : m[2]
}
}
return { protocol, value, search }
}
================================================
FILE: packages/orga/src/utils.ts
================================================
import type { PhrasingContent, Token } from './types.js'
const matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g
const escapeRegExp = (str: string): string => {
return str.replace(matchOperatorsRe, '\\$&')
}
export { escapeRegExp, escapeRegExp as escape }
export const clone = (obj: any) => {
return JSON.parse(JSON.stringify(obj))
}
export const isPhrasingContent = (token: Token): token is PhrasingContent => {
return (
token.type === 'text' ||
token.type === 'footnote.reference' ||
token.type === 'opening' ||
token.type === 'link' ||
token.type === 'newline'
)
}
================================================
FILE: packages/orga/tsconfig.json
================================================
{
"extends": "../../tsconfig.esm.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src"]
}
================================================
FILE: packages/orga-build/CHANGELOG.md
================================================
# orga-build
## 0.9.0
### Minor Changes
- 8b13493: - add exclude config option to skip files from content scanning
- decouple Vite's root from the content root (now always cwd)
## 0.8.0
### Minor Changes
- f4b8394: add data endpoint
## 0.7.1
### Patch Changes
- 850bcf9: fix: use native anchor for external links to prevent wouter pushState SecurityError
## 0.7.0
### Minor Changes
- be20652: expose rehypePlugins in orga-build
## 0.6.3
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- @orgajs/rollup@1.3.4
## 0.6.2
### Patch Changes
- 292e2f1: Use the shared virtual client entry in production builds so `styles` are imported, hashed by Vite, and injected from built CSS assets.
## 0.6.1
### Patch Changes
- b2110a4: fix index.html resolution conflict
## 0.6.0
### Minor Changes
- 18c8ed7: implement per-page head injection
## 0.5.4
### Patch Changes
- 20f5a03: fix: render video links as `` elements
## 0.5.3
### Patch Changes
- 15434f6: Normalize org file: links to canonical slugs and fix index.org link targets
## 0.5.2
### Patch Changes
- ada31b9: org-build output directly to outDir
## 0.5.1
### Patch Changes
- 23b8f16: make orga-build dev mode to be vite-native
## 0.5.0
### Minor Changes
- 06c6d43: Vit Environment API adoption
## 0.4.0
### Minor Changes
- 3a425ad: update to vite 7 🤞
## 0.3.2
### Patch Changes
- 1bff98b: fix HMR issue
## 0.3.1
### Patch Changes
- abbee9a: add docs and rename orga-build/content to orga-build/client
## 0.3.0
### Minor Changes
- ad0bd0d: Add `orga-build:content` virtual module with `getPages()`, `getPage()`, and `getEntries()` functions for querying content entries. Automatically extracts metadata from org-mode headers (e.g., `#+title:`, `#+date:`) and supports hierarchical path filtering.
## 0.2.7
### Patch Changes
- @orgajs/rollup@1.3.3
## 0.2.6
### Patch Changes
- 6d46012: remove log
## 0.2.5
### Patch Changes
- 70ebb3b: handle image and relative links
## 0.2.4
### Patch Changes
- c3fecf6: resolve react/react-dom/wouter properly
## 0.2.3
### Patch Changes
- c71a873: fix dependency issue
## 0.2.2
### Patch Changes
- 107b375: move react and react-dom to peer dependencies
## 0.2.1
### Patch Changes
- cd8358d: replace react-router with wouter
## 0.2.0
### Minor Changes
- 60ad38f: migrate orga-build to be based on vite
### Patch Changes
- Updated dependencies [60ad38f]
- @orgajs/rollup@1.3.2
- @orgajs/esbuild@1.1.3
- @orgajs/node-loader@1.1.3
## 0.1.4
### Patch Changes
- 27d31bf: remove log
## 0.1.3
### Patch Changes
- e504f45: you can refer to image using relative path now
## 0.1.2
### Patch Changes
- 10e8856: [orga-build] copy assets
## 0.1.1
### Patch Changes
- 7c3c600: fix react resolve issue
- @orgajs/esbuild@1.1.2
- @orgajs/node-loader@1.1.2
## 0.1.0
### Minor Changes
- 9392c3e: release orga-build
================================================
FILE: packages/orga-build/README.org
================================================
#+TITLE: orga-build
A simple tool that builds org-mode files into a website.
* Architecture
orga-build is built on top of Vite with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
** Key Design Principles
- *Vite-native*: Framework behavior is implemented as Vite plugins, not external server wrappers
- *Zero-config*: Works with just =.org= files - no =index.html= required
- *Plugin-driven*: All features are implemented as composable Vite plugins
** Plugin Composition
When using orga-build CLI with additional Vite plugins (like Cloudflare Workers), add them to =vitePlugins= in your =orga.config.js=:
#+begin_src javascript
// orga.config.js
import { cloudflare } from '@cloudflare/vite-plugin'
export default {
root: 'pages',
// Add external plugins here - orga-build plugins are included automatically
vitePlugins: [
cloudflare()
]
}
#+end_src
Note: Do NOT add =orgaBuildPlugin()= to =vitePlugins= - it's already included by the CLI. Only add external plugins.
For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js=:
#+begin_src javascript
// vite.config.js (advanced - direct Vite integration)
import { cloudflare } from '@cloudflare/vite-plugin'
import { orgaBuildPlugin, alias } from 'orga-build'
export default {
plugins: [
cloudflare(),
...orgaBuildPlugin({ root: 'pages', containerClass: [] })
],
resolve: { alias }
}
#+end_src
** Default HTML Template
If your project doesn't have an =index.html= file, orga-build provides a default template that:
- Sets up React rendering
- Enables client-side routing
- Works in both dev and production builds
To customize the HTML shell, create your own =index.html= in your project root.
* Installation
#+begin_src bash
npm install orga-build
#+end_src
* Configuration
orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root.
#+begin_src javascript
// orga.config.js
export default {
// Directory containing your .org files (default: 'pages')
root: 'pages',
// Output directory for production build (default: 'out')
outDir: 'out',
// CSS class(es) to wrap rendered org content
containerClass: ['prose', 'prose-lg'],
// Global stylesheets — paths relative to orga.config.js (leading / optional).
// Injected in dev SSR and imported by the client entry.
styles: ['pages/style.css'],
// Extra rehype plugins appended to orga-build defaults
// Useful for syntax highlighting (e.g. rehype-pretty-code).
rehypePlugins: [],
// Additional Vite plugins
vitePlugins: [],
// Glob patterns (relative to root) to exclude from content scanning.
// Useful for generated or declaration files that must live inside root
// but should not be treated as pages or endpoints.
exclude: ['**/*.d.ts']
}
#+end_src
** Configuration Options
| Option | Type | Default | Description |
|----------------+-------------------+---------+-----------------------------------------------------------------|
| =root= | =string= | ='pages'= | Directory containing content files |
| =outDir= | =string= | ='out'= | Output directory for production build |
| =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
| =styles= | =string[]= | =[]= | Stylesheets to inject/import; paths relative to =orga.config.js= |
| =exclude= | =string[]= | =[]= | Glob patterns (relative to =root=) excluded from content scanning |
| =rehypePlugins= | =PluggableList= | =[]= | Extra rehype plugins appended to orga-build defaults |
| =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
** Syntax Highlighting Example
#+begin_src javascript
import rehypePrettyCode from 'rehype-pretty-code'
export default {
rehypePlugins: [[rehypePrettyCode, { theme: 'github-dark' }]]
}
#+end_src
* Routing
orga-build supports two route types: *page routes* and *endpoint routes*.
** Page Routes
Page routes are discovered from =.org=, =.tsx=, and =.jsx= files.
- =index.org= -> =/=
- =about.org= -> =/about=
- =docs/getting-started.tsx= -> =/docs/getting-started=
At build time, page routes are emitted as HTML:
- =/about= -> =out/about/index.html=
** Endpoint Routes
Endpoint routes are discovered from =.ts=, =.js=, =.mts=, and =.mjs= files where the basename already includes a target extension (for example =rss.xml.ts= or =data.json.ts=).
- =rss.xml.ts= -> =/rss.xml=
- =nested/feed.xml.ts= -> =/nested/feed.xml=
- =api/data.json.ts= -> =/api/data.json=
Endpoint modules must export:
#+begin_src ts
export async function GET(ctx) {
return new Response('ok', {
headers: { 'content-type': 'text/plain; charset=utf-8' }
})
}
#+end_src
At build time, endpoint routes are emitted to exact filenames:
- =/rss.xml= -> =out/rss.xml=
- =/api/data.json= -> =out/api/data.json=
Route conflicts (same final route path) fail fast during dev/build startup.
* TypeScript Setup
If you're using TypeScript and want type support for the =orga-build:content= virtual module, you need to add a reference to the type definitions.
** Minimal Setup
1. Create a =types.d.ts= file in your project root (or any location):
#+begin_src typescript
///
#+end_src
2. Ensure your =tsconfig.json= includes this file:
#+begin_src json
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx"
},
"include": ["types.d.ts", "**/*"]
}
#+end_src
That's it! TypeScript will now recognize imports from =orga-build:content=.
** Why This is Needed
The =orga-build:content= module is a "virtual module" - it doesn't exist as a physical file but is generated at build time by Vite. The =/// = directive tells TypeScript to load the type definitions for this virtual module.
* Content Query API
orga-build provides an Astro-inspired content query API via the =orga-build:content= virtual module. This allows you to safely query content entries from any page or layout without circular imports.
** Importing
#+begin_src typescript
import { getPages, getPage } from 'orga-build:content'
#+end_src
** API Reference
*** =getPages(path?, filter?)=
Get all content entries matching a path pattern.
*Parameters:*
- =path= (optional): Path prefix to filter by (e.g., ='writing'=, ='content/writing/2025'=)
- =filter= (optional): Filter function to further refine results
*Returns:* Array of =ContentEntry= objects
*Examples:*
#+begin_src typescript
// Get all entries
const all = getPages()
// Get all entries in the 'writing' path
const writing = getPages('writing')
// Get entries in a nested path
const posts2025 = getPages('content/writing/2025')
// Filter out drafts
const published = getPages('writing', (entry) => {
return entry.data['draft'] !== 'true'
})
#+end_src
*** =getPage(idOrSlug, path?)=
Get a single content entry by id or slug.
*Parameters:*
- =idOrSlug=: The id or slug of the entry to find
- =path= (optional): Path prefix to search within
*Returns:* =ContentEntry | undefined=
*Examples:*
#+begin_src typescript
// Get by slug
const post = getPage('/writing/the-birth-of-emacsclient')
// Get by id within a path
const post = getPage('the-birth-of-emacsclient', 'writing')
#+end_src
*** =getEntries(refs)=
Get multiple content entries by reference.
*Parameters:*
- =refs=: Array of references with =id= and optional =path=
*Returns:* Array of =ContentEntry | undefined=
*Examples:*
#+begin_src typescript
const entries = getEntries([
{ id: 'post-1', path: 'writing' },
{ id: 'post-2', path: 'writing' }
])
#+end_src
*** Aliases
For Astro familiarity:
- =getCollection= - Alias for =getPages=
- =getEntry= - Alias for =getPage=
** ContentEntry Type
Each entry has the following structure:
#+begin_src typescript
interface ContentEntry {
id: string // e.g., 'post-name' or 'index'
slug: string // e.g., '/writing/post-name'
path: string // e.g., 'writing' or 'content/writing/2025'
filePath: string // absolute source file path
ext: 'org' | 'tsx' | 'jsx' // file extension
data: Record // metadata from org headers
}
#+end_src
** Metadata Extraction
For =.org= files, orga-build automatically extracts metadata from org-mode headers:
#+begin_example
#+title: My Post
#+date: 2025-01-15
#+draft: false
#+end_example
This becomes:
#+begin_src typescript
{
data: {
title: 'My Post',
date: '2025-01-15',
draft: 'false'
}
}
#+end_src
For =.tsx= and =.jsx= files, the =data= field is currently empty in v1.
** Path Matching Behavior
Path matching works hierarchically:
- =getPages()= - Returns all entries
- =getPages('writing')= - Returns entries where:
- =path = 'writing'=, or
- =path= starts with ='writing/'=
- =getPages('content/writing/2025')= - Returns entries under that specific subtree
*Path Derivation Examples:*
| File Path | Slug | Path | ID |
|-----------+------+------+----|
| =pages/writing/foo.org= | =/writing/foo= | =writing= | =foo= |
| =pages/content/writing/2025/post.org= | =/content/writing/2025/post= | =content/writing/2025= | =post= |
| =pages/index.org= | =/= | =''= (empty) | =index= |
| =pages/about.org= | =/about= | =''= (empty) | =about= |
** Example: Blog Index Page
#+begin_src tsx
import { getPages } from 'orga-build:content'
export default function BlogIndex() {
const posts = getPages('writing', (entry) => {
return entry.data['draft'] !== 'true'
}).sort((a, b) => {
// Sort by date descending
return String(b.data['date']).localeCompare(String(a.data['date']))
})
return (
)
}
#+end_src
** Example: Related Posts
#+begin_src tsx
import { getPages } from 'orga-build:content'
export default function Post({ slug }: { slug: string }) {
// Get current post
const currentPost = getPages().find((p) => p.slug === slug)
// Get related posts from same path
const related = getPages(currentPost?.path).filter(
(p) => p.slug !== slug
).slice(0, 3)
return (
)
}
#+end_src
* Development
** TODO Items
- resolve relative path in links and images
- monitor file changes and cache properly
* License
MIT
================================================
FILE: packages/orga-build/cli.js
================================================
#!/usr/bin/env node
import { argv } from 'node:process'
import { parseArgs } from 'node:util'
import { build } from './lib/build.js'
import { loadConfig } from './lib/config.js'
import { serve } from './lib/serve.js'
const { positionals } = parseArgs({
args: argv.slice(2),
options: {
watch: { type: 'boolean', short: 'w' },
outDir: { type: 'string', short: 'o', default: '.out' }
},
tokens: true,
allowPositionals: true
})
const { config, projectRoot } = await loadConfig(
'orga.config.js',
'orga.config.mjs'
)
await (positionals.includes('dev')
? serve(config, 3000, projectRoot)
: build(config, projectRoot))
================================================
FILE: packages/orga-build/index.js
================================================
export { build } from './lib/build.js'
export { alias, createOrgaBuildConfig, orgaBuildPlugin } from './lib/plugin.js'
================================================
FILE: packages/orga-build/lib/__tests__/build.test.js
================================================
import assert from 'node:assert'
import fs from 'node:fs/promises'
import path from 'node:path'
import { after, before, describe, test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { build } from '../build.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const fixtureDir = path.join(__dirname, 'fixtures')
const outDir = path.join(__dirname, '.test-output')
function markCodeBlocks() {
/**
* @param {any} tree
*/
return (tree) => {
tree.children ||= []
tree.children.unshift({
type: 'element',
tagName: 'div',
properties: { id: 'rehype-plugin-ran' },
children: []
})
}
}
describe('orga-build', () => {
before(async () => {
await fs.mkdir(fixtureDir, { recursive: true })
await fs.mkdir(path.join(fixtureDir, 'docs'), { recursive: true })
// Create minimal fixture
await fs.writeFile(
path.join(fixtureDir, 'index.org'),
`#+title: Test Page
* Hello World
This is a test page.
Here's [[file:./docs/index.org][index page]].
Here's [[file:more.org][another page]].
Here's [[mailto:hi@unclex.net][send me an email]].
`
)
await fs.writeFile(
path.join(fixtureDir, 'docs', 'index.org'),
'Docs index page.'
)
await fs.writeFile(path.join(fixtureDir, 'more.org'), 'Another page.')
await fs.writeFile(
path.join(fixtureDir, 'rss.xml.ts'),
`import { getPages } from 'orga-build:content'
export function GET() {
const pages = getPages()
return new Response(
'' + pages.length + ' ',
{ headers: { 'content-type': 'application/xml; charset=utf-8' } }
)
}
`
)
await fs.writeFile(
path.join(fixtureDir, 'style.css'),
'.global-style-marker { color: rgb(1, 2, 3); }'
)
})
after(async () => {
await fs.rm(outDir, { recursive: true, force: true })
await fs.rm(fixtureDir, { recursive: true, force: true })
})
test('builds org files to HTML', async () => {
await build({
root: fixtureDir,
outDir: outDir,
containerClass: [],
vitePlugins: [],
preBuild: [],
postBuild: []
})
// Check output exists
const indexPath = path.join(outDir, 'index.html')
const indexExists = await fs
.access(indexPath)
.then(() => true)
.catch(() => false)
assert.ok(indexExists, 'index.html should exist')
// Check content
const html = await fs.readFile(indexPath, 'utf-8')
assert.ok(html.includes('Test Page '), 'should have title')
assert.ok(html.includes('Hello World'), 'should have heading content')
assert.ok(
html.includes('href="/docs"'),
'should rewrite docs/index.org to /docs'
)
assert.ok(html.includes('href="/more"'), 'should rewrite more.org to /more')
assert.ok(
html.includes('href="mailto:hi@unclex.net"'),
'should keep mailto protocol in href'
)
})
test('generates assets directory', async () => {
const assetsDir = path.join(outDir, 'assets')
const assetsExists = await fs
.access(assetsDir)
.then(() => true)
.catch(() => false)
assert.ok(assetsExists, 'assets directory should exist')
})
test('processes configured global styles through vite and injects built css', async () => {
const styleUrl =
'/' + path.relative(process.cwd(), path.join(fixtureDir, 'style.css'))
await build({
root: fixtureDir,
outDir: outDir,
containerClass: [],
styles: [styleUrl],
vitePlugins: [],
preBuild: [],
postBuild: []
})
const html = await fs.readFile(path.join(outDir, 'index.html'), 'utf-8')
assert.ok(
!html.includes('href="/style.css"'),
'should not link raw source css path'
)
const cssHrefMatch = html.match(/href="\/(assets\/[^"]+\.css)"/)
assert.ok(
cssHrefMatch,
'should link built css asset from assets with hashed name'
)
const builtCssPath = cssHrefMatch[1]
const builtCss = await fs.readFile(path.join(outDir, builtCssPath), 'utf-8')
assert.ok(
builtCss.includes('.global-style-marker'),
'built css should include configured global style content'
)
})
test('applies custom rehype plugins from config', async () => {
const fixtureDirRehype = path.join(__dirname, 'fixtures-rehype')
const outDirRehype = path.join(__dirname, '.test-output-rehype')
try {
await fs.mkdir(fixtureDirRehype, { recursive: true })
await fs.writeFile(
path.join(fixtureDirRehype, 'index.org'),
`#+title: Rehype Test
This page verifies custom rehype plugins.`
)
await build({
root: fixtureDirRehype,
outDir: outDirRehype,
containerClass: [],
rehypePlugins: [markCodeBlocks],
vitePlugins: [],
preBuild: [],
postBuild: []
})
const html = await fs.readFile(
path.join(outDirRehype, 'index.html'),
'utf-8'
)
assert.ok(
html.includes('rehype-plugin-ran'),
'should apply user-provided rehype plugins to rendered HTML'
)
} finally {
await fs.rm(outDirRehype, { recursive: true, force: true })
await fs.rm(fixtureDirRehype, { recursive: true, force: true })
}
})
test('emits endpoint routes with exact output filenames', async () => {
await build({
root: fixtureDir,
outDir: outDir,
containerClass: [],
vitePlugins: [],
preBuild: [],
postBuild: []
})
const rss = await fs.readFile(path.join(outDir, 'rss.xml'), 'utf-8')
assert.ok(
rss.includes('') && rss.includes(''),
'should emit rss.xml from GET endpoint'
)
})
test('fails on duplicate route conflicts', async () => {
const fixtureDirConflict = path.join(__dirname, 'fixtures-conflict')
const outDirConflict = path.join(__dirname, '.test-output-conflict')
try {
await fs.mkdir(fixtureDirConflict, { recursive: true })
await fs.writeFile(path.join(fixtureDirConflict, 'index.org'), 'Home')
await fs.writeFile(
path.join(fixtureDirConflict, 'index.tsx'),
'export default function Page() { return Index
}'
)
await assert.rejects(
() =>
build({
root: fixtureDirConflict,
outDir: outDirConflict,
containerClass: [],
vitePlugins: [],
preBuild: [],
postBuild: []
}),
/Route conflict detected/
)
} finally {
await fs.rm(outDirConflict, { recursive: true, force: true })
await fs.rm(fixtureDirConflict, { recursive: true, force: true })
}
})
})
================================================
FILE: packages/orga-build/lib/app.jsx
================================================
import { Link, Route, Switch } from 'wouter'
import * as components from '/@orga-build/components'
import layouts from '/@orga-build/layouts'
import pages from '/@orga-build/pages'
function SmartLink({ href, ...props }) {
if (!href || /^([a-z][a-z\d+\-.]*:|\/\/)/i.test(href)) {
return
}
return
}
export function App() {
const _pages = Object.entries(pages).map(([path, page]) => {
return {
slug: path,
...page
}
})
const pageRoutes = Object.entries(pages).map(([path, page]) => {
const layoutIds = Object.keys(layouts)
.filter((key) => path.startsWith(key))
.sort((a, b) => -a.localeCompare(b))
let element = (
)
for (const layoutId of layoutIds) {
const Layout = layouts[layoutId]
element = (
{element}
)
}
return {
path,
element
}
})
return (
{pageRoutes.map((route) => {
return (
{route.element}
)
})}
)
}
================================================
FILE: packages/orga-build/lib/build.js
================================================
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createBuilder } from 'vite'
import { resolveEndpointResponse } from './endpoint.js'
import { emptyDir, ensureDir, exists } from './fs.js'
import { alias, createOrgaBuildConfig } from './plugin.js'
import { escapeHtml } from './util.js'
import { appEntryId } from './vite.js'
// Re-export alias for backwards compatibility
export { alias }
const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
/**
* @param {import('./config.js').Config} config
* @param {string} [projectRoot]
*/
export async function build(
{
outDir,
root,
containerClass,
styles = [],
rehypePlugins = [],
vitePlugins = [],
exclude = []
},
projectRoot = process.cwd()
) {
await emptyDir(outDir)
const ssrOutDir = path.join(outDir, '.ssr')
const clientOutDir = outDir
const { plugins, resolve } = createOrgaBuildConfig({
root,
outDir,
containerClass,
styles,
rehypePlugins,
vitePlugins,
exclude
})
// Shared config with environment-specific build settings
const builder = await createBuilder({
plugins,
resolve,
ssr: { noExternal: true },
environments: {
ssr: {
build: {
ssr: true,
outDir: ssrOutDir,
cssCodeSplit: false,
emptyOutDir: true,
minify: false,
rollupOptions: {
input: ssrEntry,
output: {
entryFileNames: '[name].mjs',
chunkFileNames: '[name]-[hash].mjs'
}
}
}
},
client: {
build: {
outDir: clientOutDir,
cssCodeSplit: false,
emptyOutDir: false,
assetsDir: 'assets',
rollupOptions: {
input: appEntryId,
preserveEntrySignatures: 'allow-extension'
}
}
}
}
})
// Build SSR first to get render function and pages
console.log('preparing ssr bundle...')
await builder.build(builder.environments.ssr)
const {
render,
pages,
endpoints = {}
} = await import(pathToFileURL(path.join(ssrOutDir, 'ssr.mjs')).toString())
// Build client bundle
const _clientResult = await builder.build(builder.environments.client)
// Normalize build result to single RollupOutput
const clientOutput = Array.isArray(_clientResult)
? _clientResult[0].output
: 'output' in _clientResult
? _clientResult.output
: null
if (!clientOutput) throw new Error('Unexpected client build result')
/* --- get from client bundle result: entry chunk, css chunks --- */
const entryChunk = clientOutput.find(
(/** @type {any} */ c) => c.type === 'chunk' && c.isEntry
)
const cssChunks = clientOutput.filter(
(/** @type {any} */ c) => c.type === 'asset' && c.fileName.endsWith('.css')
)
/* --- get html template, inject entry js and css --- */
// Check for user's index.html in project root, otherwise use default
const userIndexPath = path.join(projectRoot, 'index.html')
const indexHtmlPath = (await exists(userIndexPath))
? userIndexPath
: defaultIndexHtml
const template = await fs.readFile(indexHtmlPath, { encoding: 'utf-8' })
/* --- for each page path, render html using render function from ssr bundle, and inject the right css --- */
const pagePaths = Object.keys(pages)
await Promise.all(
pagePaths.map(async (pagePath) => {
const html = renderHTML(pagePath)
const writePath = path.join(
clientOutDir,
pagePath.replace(/^\//, ''),
'index.html'
)
await ensureDir(path.dirname(writePath))
await fs.writeFile(writePath, html)
})
)
const endpointPaths = Object.keys(endpoints)
await Promise.all(
endpointPaths.map(async (route) => {
const endpointModule = endpoints[route]
const ctx = {
url: new URL(`http://localhost${route}`),
params: {},
mode: /** @type {'build'} */ ('build'),
route: { route }
}
const response = await resolveEndpointResponse(endpointModule, ctx, 'GET')
if (response.status < 200 || response.status >= 300) {
throw new Error(
`Endpoint route "${route}" returned non-2xx status during build: ${response.status}`
)
}
const bytes = Buffer.from(await response.arrayBuffer())
const writePath = path.join(clientOutDir, route.replace(/^\//, ''))
await ensureDir(path.dirname(writePath))
await fs.writeFile(writePath, bytes)
})
)
await fs.rm(ssrOutDir, { recursive: true })
return
// ---------- the end ----------
/**
* @param {string} pagePath
*/
function renderHTML(pagePath) {
const content = render(pagePath)
const ssr = {
routePath: pagePath
}
let html = template.replace(
'
',
`
${content}
`
)
const css = cssChunks
.map(
(/** @type {any} */ c) =>
` `
)
.join('\n')
html = html.replace(
'',
``
)
html = html.replace('', `${css}`)
const page = pages[pagePath]
if (page) {
html = html.replace(/%orga\.(\w+)%/g, (_, key) => {
const value = page[key] ?? ''
return escapeHtml(String(value))
})
}
return html
}
}
================================================
FILE: packages/orga-build/lib/components.js
================================================
export { Link } from 'wouter'
================================================
FILE: packages/orga-build/lib/config.js
================================================
import fs from 'node:fs/promises'
import path from 'node:path'
/**
* @typedef {Object} Config
* @property {string} outDir
* @property {string} root
* @property {string[]} preBuild
* @property {string[]} postBuild
* @property {import('vite').PluginOption[]} vitePlugins - Array of Vite plugins
* @property {string[]|string} containerClass
* @property {string[]} [styles] - Global stylesheet URLs injected in dev SSR and imported by client entry
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
*/
/** @type {Config} */
const defaultConfig = {
outDir: '.out',
root: '.',
preBuild: [],
postBuild: [],
vitePlugins: [],
containerClass: [],
styles: [],
rehypePlugins: [],
exclude: []
}
/**
* @param {string[]} files
* @returns {Promise<{ config: Config, projectRoot: string }>}
*/
export async function loadConfig(...files) {
const cwd = process.cwd()
/**
* @param {string} value
*/
const resolveConfigPath = (value) =>
path.isAbsolute(value) ? value : path.resolve(cwd, value)
let result = { ...defaultConfig }
let configPath = null
for (const file of files) {
const filePath = path.join(cwd, file)
try {
await fs.access(filePath, fs.constants.F_OK)
} catch {
// File doesn't exist, try next
continue
}
try {
const module = await import(filePath)
// Support both default export (recommended) and named exports
const config = module.default || module
result = { ...defaultConfig, ...config }
configPath = filePath
break
} catch (err) {
// Config file exists but has errors
console.error(`Error loading config from ${file}:`, err)
}
}
result.root = resolveConfigPath(result.root)
result.outDir = resolveConfigPath(result.outDir)
const styles = result.styles
result.styles = Array.isArray(styles)
? styles
.filter((v) => typeof v === 'string')
.map((v) => '/' + v.replace(/^\/+/, ''))
: []
return {
config: result,
projectRoot: configPath ? path.dirname(configPath) : cwd
}
}
================================================
FILE: packages/orga-build/lib/content.d.ts
================================================
declare module 'orga-build:content' {
export interface ContentEntry {
id: string
slug: string
path: string
filePath: string
ext: 'org' | 'tsx' | 'jsx'
data: Record
}
/**
* Get all content entries matching a path pattern
* @param path - Optional path prefix to filter by (e.g., 'writing', 'content/writing/2025')
* @param filter - Optional filter function to further refine results
* @returns Array of matching content entries
*/
export function getPages(
path?: string,
filter?: (entry: ContentEntry) => boolean
): ContentEntry[]
/**
* Get a single content entry by id or slug
* @param idOrSlug - The id or slug of the entry to find
* @param path - Optional path prefix to search within
* @returns The matching content entry or undefined
*/
export function getPage(
idOrSlug: string,
path?: string
): ContentEntry | undefined
/**
* Get multiple content entries by reference
* @param refs - Array of references with id and optional path
* @returns Array of matching content entries (may include undefined)
*/
export function getEntries(
refs: Array<{ path?: string; id: string }>
): Array
/**
* Alias for getPages
*/
export const getCollection: typeof getPages
/**
* Alias for getPage
*/
export const getEntry: typeof getPage
}
================================================
FILE: packages/orga-build/lib/csr.jsx
================================================
import { createRoot } from 'react-dom/client'
import { Router } from 'wouter'
import { App } from './app.jsx'
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
)
================================================
FILE: packages/orga-build/lib/endpoint.js
================================================
/**
* @typedef {Object} EndpointContext
* @property {URL} url
* @property {Record} params
* @property {'dev' | 'build'} mode
* @property {{ route: string }} route
*/
/**
* @param {Record} endpointModule
* @param {EndpointContext} ctx
* @param {string} method
* @returns {Promise}
*/
export async function resolveEndpointResponse(
endpointModule,
ctx,
method = 'GET'
) {
const route = ctx.route.route
if (method === 'HEAD' && typeof endpointModule.HEAD === 'function') {
const res = await endpointModule.HEAD(ctx)
if (!(res instanceof Response))
throw new Error(`Endpoint route "${route}" HEAD must return Response`)
return res
}
if (typeof endpointModule.GET !== 'function') {
throw new Error(
`Endpoint route "${route}" must export GET(ctx) returning Response`
)
}
const res = await endpointModule.GET(ctx)
if (!(res instanceof Response)) {
throw new Error(`Endpoint route "${route}" GET must return Response`)
}
if (method === 'HEAD') {
return new Response(null, {
status: res.status,
statusText: res.statusText,
headers: new Headers(res.headers)
})
}
return res
}
================================================
FILE: packages/orga-build/lib/files.js
================================================
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { globby } from 'globby'
import { getSettings } from 'orga'
/**
* @typedef {Object} Page
* @property {string} dataPath
* @property {string} [title]
* Path to the page data file
*/
/**
* @typedef {Object} EndpointRoute
* @property {string} route
* @property {string} dataPath
*/
/**
* @typedef {Object} ContentEntry
* @property {string} id
* @property {string} slug
* @property {string} path
* @property {string} filePath
* @property {'org' | 'tsx' | 'jsx'} ext
* @property {Record} data
*/
/**
* Extract file extension from file path
* @param {string} filePath
* @returns {'org' | 'tsx' | 'jsx'}
*/
function getFileExtension(filePath) {
const match = filePath.match(/\.(org|tsx|jsx)$/)
return /** @type {'org' | 'tsx' | 'jsx'} */ (match ? match[1] : 'org')
}
/**
* Derive content path from slug
* @param {string} slug - e.g. '/writing/foo', '/content/writing/2025/post', '/'
* @returns {string} - e.g. 'writing', 'content/writing/2025', ''
*/
function getContentPath(slug) {
// Remove leading slash
let normalized = slug.replace(/^\/+/, '')
// Remove trailing slash
normalized = normalized.replace(/\/+$/, '')
// If root page, return empty string
if (!normalized) {
return ''
}
// Get all segments except the last one (the file name)
const segments = normalized.split('/')
if (segments.length === 1) {
// Single segment like '/about' -> path is empty (root level)
return ''
}
// Multiple segments like '/writing/foo' -> path is 'writing'
return segments.slice(0, -1).join('/')
}
/**
* Derive content id from slug
* @param {string} slug - e.g. '/writing/foo', '/writing', '/'
* @returns {string} - e.g. 'foo', 'writing', 'index'
*/
function getContentId(slug) {
// Remove leading and trailing slashes
const normalized = slug.replace(/^\/+/, '').replace(/\/+$/, '')
// If root page, return 'index'
if (!normalized) {
return 'index'
}
// Get last segment
const segments = normalized.split('/')
return segments[segments.length - 1] || 'index'
}
/**
* @param {string} dir
* @param {object} [options]
* @param {string} [options.outDir] - Output directory to exclude from file discovery
* @param {string[]} [options.exclude] - Additional glob patterns to exclude from file discovery
*/
export function setup(dir, { outDir, exclude = [] } = {}) {
const outDirRelative = outDir ? path.relative(dir, outDir) : null
// Only exclude outDir if it's inside the root (not an external path like ../out)
const outDirExclude =
outDirRelative && !outDirRelative.startsWith('..')
? `!${outDirRelative}/**`
: null
const discoveredRoutes = cache(async function () {
const files = await globby(
[
'**/*.{org,tsx,jsx,ts,js,mts,mjs}',
'!**/_*/**',
'!**/_*',
'!**/.*/**',
'!**/.*',
'!node_modules/**',
...(outDirExclude ? [outDirExclude] : []),
...exclude.map((p) => `!${p}`)
],
{ cwd: dir }
)
/** @type {Record} */
const pages = {}
/** @type {Record} */
const endpoints = {}
/** @type {Map} */
const routeOwners = new Map()
for (const file of files) {
const absolutePath = path.join(dir, file)
const pageSlug = getPageSlugFromFilePath(file)
const endpointRoute = getEndpointRouteFromFilePath(file)
if (pageSlug) {
assertUniqueRoute(routeOwners, pageSlug, 'page', absolutePath)
pages[pageSlug] = { dataPath: absolutePath }
}
if (endpointRoute) {
assertUniqueRoute(routeOwners, endpointRoute, 'endpoint', absolutePath)
endpoints[endpointRoute] = {
route: endpointRoute,
dataPath: absolutePath
}
}
}
return { pages, endpoints }
})
const pages = cache(async function () {
const routes = await discoveredRoutes()
return routes.pages
})
const endpoints = cache(async function () {
const routes = await discoveredRoutes()
return routes.endpoints
})
const layouts = cache(async function () {
const layoutFiles = await globby(
[
'**/_layout.{tsx,jsx}',
'!**/.*/**',
'!**/.*',
'!node_modules/**',
...(outDirExclude ? [outDirExclude] : []),
...exclude.map((p) => `!${p}`)
],
{
cwd: dir
}
)
return layoutFiles.reduce(
(/** @type {Record} */ result, file) => {
const layoutSlug = path.dirname(getSlugFromContentFilePath(file))
result[layoutSlug] = path.join(dir, file)
return result
},
/** @type {Record} */ {}
)
})
const components = cache(async function () {
const files = await globby(
[
'_components.{tsx,jsx}',
'!**/.*/**',
'!**/.*',
'!node_modules/**',
...(outDirExclude ? [outDirExclude] : []),
...exclude.map((p) => `!${p}`)
],
{
cwd: dir
}
)
return files[0] ? path.join(dir, files[0]) : null
})
const contentEntries = cache(async function () {
const allPages = await pages()
/** @type {ContentEntry[]} */
const entries = []
for (const [slug, pageData] of Object.entries(allPages)) {
const filePath = pageData.dataPath
const ext = getFileExtension(filePath)
// Derive path from directory structure
const derivedPath = getContentPath(slug)
// Derive id from the slug (last segment or 'index')
const id = getContentId(slug)
/** @type {Record} */
let data = {}
// Extract metadata from .org files
if (ext === 'org') {
try {
const content = await readFile(filePath, 'utf-8')
data = getSettings(content)
} catch (/** @type {any} */ error) {
console.warn(
`Failed to read metadata from ${filePath}:`,
error?.message || error
)
}
}
entries.push({
id,
slug,
path: derivedPath,
filePath,
ext,
data
})
}
return entries
})
const files = {
pages,
page,
endpoints,
endpoint,
components,
layouts,
contentEntries,
invalidate() {
discoveredRoutes.invalidate()
pages.invalidate()
endpoints.invalidate()
layouts.invalidate()
components.invalidate()
contentEntries.invalidate()
}
}
return files
/** @param {string} slug */
async function page(slug) {
const all = await pages()
return all[slug]
}
/** @param {string} route */
async function endpoint(route) {
const all = await endpoints()
return all[route]
}
}
/**
* Creates a cached version of an async function that will only execute once
* and return the cached result on subsequent calls. The returned function
* also has an `invalidate()` method to clear the cache.
*
* @template T
* @param {() => Promise} fn - The async function to cache
* @returns {(() => Promise) & { invalidate: () => void }}
*/
function cache(fn) {
let settled = false
/** @type {T | undefined} */
let value
/** @returns {Promise} */
async function cached() {
if (!settled) {
value = await fn()
settled = true
}
return /** @type {T} */ (value)
}
cached.invalidate = function () {
settled = false
value = undefined
}
return cached
}
/**
* Convert a content file path (relative to content root) to the canonical page slug.
* @param {string} contentFilePath
*/
export function getSlugFromContentFilePath(contentFilePath) {
const normalizedFilePath = contentFilePath.replace(/\\/g, '/')
let slug = normalizedFilePath.replace(/\.(org|md|mdx|js|jsx|ts|tsx)$/, '')
slug = slug.replace(/index$/, '')
// remove trailing slash
slug = slug.replace(/\/$/, '')
// ensure starting slash
slug = slug.replace(/^\//, '')
slug = `/${slug}`
// turn [id] into :id
// so that react-router can recognize it as url params
// pagePublicPath = pagePublicPath.replace(
// /\[(.*?)\]/g,
// (_, paramName) => `:${paramName}`
// )
return slug
}
/**
* @param {string} filePath
* @returns {string|null}
*/
function getPageSlugFromFilePath(filePath) {
if (!/\.(org|tsx|jsx)$/.test(filePath)) {
return null
}
return getSlugFromContentFilePath(filePath)
}
/**
* @param {string} filePath
* @returns {string|null}
*/
function getEndpointRouteFromFilePath(filePath) {
if (!/\.(ts|js|mts|mjs)$/.test(filePath)) {
return null
}
const normalizedFilePath = filePath.replace(/\\/g, '/')
const targetPath = normalizedFilePath.replace(/\.(ts|js|mts|mjs)$/, '')
const basename = path.posix.basename(targetPath)
// Endpoint files must carry a target extension: rss.xml.ts, data.json.ts, etc.
if (!basename.includes('.')) {
return null
}
return `/${targetPath.replace(/^\/+/, '')}`
}
/**
* @param {Map} routeOwners
* @param {string} route
* @param {'page' | 'endpoint'} sourceType
* @param {string} filePath
*/
function assertUniqueRoute(routeOwners, route, sourceType, filePath) {
const existing = routeOwners.get(route)
if (existing) {
throw new Error(
`Route conflict detected for "${route}"\n- ${existing.sourceType}: ${existing.filePath}\n- ${sourceType}: ${filePath}`
)
}
routeOwners.set(route, { sourceType, filePath })
}
================================================
FILE: packages/orga-build/lib/fs.js
================================================
import fs from 'node:fs/promises'
import path from 'node:path'
/**
* Resolves a path to an absolute path, even if it's already absolute
* @param {string} rootPath - The path to resolve
* @returns {string} - The absolute path
*/
export function resolvePath(rootPath) {
if (!rootPath) {
return process.cwd()
}
// If it's already absolute, return it as is
if (path.isAbsolute(rootPath)) {
return rootPath
}
// Otherwise, resolve it relative to the current working directory
return path.resolve(process.cwd(), rootPath)
}
/**
* @param {string} path
* @returns {Promise}
*/
export async function exists(path) {
try {
await fs.access(path)
return true
} catch {
return false
}
}
/**
* @param {string} dir
* @returns {Promise}
*/
export async function emptyDir(dir) {
/** @type {string[]} */
let items = []
try {
items = await fs.readdir(dir)
} catch {
await fs.mkdir(dir, { recursive: true })
}
await Promise.all(
items.map((item) => fs.rm(`${dir}/${item}`, { recursive: true }))
)
}
/**
* @param {string} path
*/
export async function ensureDir(path) {
try {
await fs.mkdir(path, { recursive: true })
} catch (/** @type{any} */ e) {
if (e && typeof e === 'object' && 'code' in e && e.code !== 'EEXIST') {
throw e
}
}
}
/**
* @param {string} src
* @param {string} dest
*/
export async function copy(src, dest) {
await fs.mkdir(dest, { recursive: true })
await fs.cp(src, dest, { recursive: true })
}
================================================
FILE: packages/orga-build/lib/index.html
================================================
%orga.title%
================================================
FILE: packages/orga-build/lib/orga.js
================================================
/**
* @import {Root as HastTree} from 'hast'
*/
import path from 'node:path'
import _orga from '@orgajs/rollup'
import { visitParents } from 'unist-util-visit-parents'
import { getSlugFromContentFilePath } from './files.js'
/**
* @param {Object} options
* @param {string|string[]} options.containerClass - CSS class name(s) to wrap the rendered content
* @param {string} options.root - Root directory for content files
* @param {import('unified').PluggableList} [options.rehypePlugins] - Extra rehype plugins appended to defaults
*/
export function setupOrga({ containerClass, root, rehypePlugins = [] }) {
return _orga({
rehypePlugins: [
[rehypeWrap, { className: containerClass }],
[rewriteOrgFileLinks, { root }],
mediaAssets,
...rehypePlugins
]
})
}
// --- plugins ---
function mediaAssets() {
/**
* @param {any} tree
*/
return function (tree) {
/** @type {Record} */
const imports = {}
visitParents(tree, [{ tagName: 'img' }, { tagName: 'video' }], (node) => {
node.type = 'jsx'
const { src, ...rest } = node.properties
if (typeof src !== 'string') return
if (src.startsWith('http')) return
const tagName = node.tagName
if (!imports[src]) imports[src] = `asset_${genId()}`
const name = imports[src]
const attrs = Object.entries(rest)
.filter(([, v]) => v !== undefined && v !== false)
.map(([k, v]) => (v === true ? k : `${k}='${v}'`))
.join(' ')
node.value = `<${tagName} src={${name}}${attrs ? ` ${attrs}` : ''}/>`
})
for (const [src, name] of Object.entries(imports)) {
tree.children.unshift({
type: 'jsx',
value: `import ${name} from '${src}'`,
children: []
})
}
}
}
/**
* @param {Object} options
* @param {string[]} options.className
*/
function rehypeWrap({ className = [] }) {
/**
* Transform.
*
* @param {HastTree} tree
* Tree.
* @returns {HastTree}
* Nothing.
*/
return (tree) => {
return {
...tree,
children: [
{
type: 'element',
tagName: 'div',
properties: {
className
},
// @ts-expect-error
children: tree.children
}
]
}
}
}
/**
* @param {Object} options
* @param {string} options.root
*/
function rewriteOrgFileLinks({ root }) {
/**
* @param {any} tree
* @param {import('vfile').VFile} [file]
*/
return function (tree, file) {
const filePath = file?.path
if (!filePath) return
visitParents(tree, { tagName: 'a' }, (node) => {
const href = node?.properties?.href
if (typeof href !== 'string') return
if (!href.endsWith('.org')) return
const targetSlug = resolveOrgHrefToContentSlug({
root,
filePath,
href
})
if (!targetSlug) return
node.properties.href = targetSlug
})
}
}
/**
* @param {Object} options
* @param {string} options.root
* @param {string} options.filePath
* @param {string} options.href
* @returns {string|null}
*/
function resolveOrgHrefToContentSlug({ root, filePath, href }) {
const decodedHrefPath = decodeURI(href)
const absoluteTargetPath = decodedHrefPath.startsWith('/')
? path.resolve(root, `.${decodedHrefPath}`)
: path.resolve(path.dirname(filePath), decodedHrefPath)
const relativeTargetPath = path.relative(root, absoluteTargetPath)
if (
relativeTargetPath.startsWith('..') ||
path.isAbsolute(relativeTargetPath)
) {
return null
}
return getSlugFromContentFilePath(relativeTargetPath)
}
function genId(length = 8) {
const array = new Uint8Array(length)
crypto.getRandomValues(array)
return Array.from(array, (byte) => (byte % 36).toString(36)).join('')
}
================================================
FILE: packages/orga-build/lib/plugin.js
================================================
import fs from 'node:fs/promises'
import { createRequire } from 'node:module'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import { createServerModuleRunner } from 'vite'
import { resolveEndpointResponse } from './endpoint.js'
import { setupOrga } from './orga.js'
import { escapeHtml } from './util.js'
import { pluginFactory } from './vite.js'
const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
const require = createRequire(import.meta.url)
const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
/**
* Alias map for React and wouter to ensure consistent resolution
*/
export const alias = {
react: path.dirname(require.resolve('react/package.json')),
'react-dom': path.dirname(require.resolve('react-dom/package.json')),
wouter: path.dirname(require.resolve('wouter'))
}
/**
* @typedef {Object} OrgaBuildPluginOptions
* @property {string} root - Root directory for content files
* @property {string | undefined} [outDir] - Output directory (excluded from file discovery)
* @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
* @property {string[]} [styles] - Global stylesheet URLs to import/inject
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
*/
/**
* Creates the canonical orga-build plugin preset.
* This is the single composition path used by both dev and build.
*
* @param {OrgaBuildPluginOptions} options
* @returns {import('vite').PluginOption[]}
*/
export function orgaBuildPlugin({
root,
outDir,
containerClass = [],
styles = [],
rehypePlugins = [],
exclude = []
}) {
return [
setupOrga({ containerClass, root, rehypePlugins }),
react(),
pluginFactory({ dir: root, outDir, styles, exclude })
]
}
/**
* Creates the full Vite config options for orga-build.
* Includes plugins, resolve aliases, and other shared config.
*
* @param {OrgaBuildPluginOptions & { outDir?: string, vitePlugins?: import('vite').PluginOption[], includeFallbackHtml?: boolean, projectRoot?: string }} options
* @returns {{ plugins: import('vite').PluginOption[], resolve: { alias: typeof alias } }}
*/
export function createOrgaBuildConfig({
root,
outDir,
containerClass = [],
styles = [],
rehypePlugins = [],
vitePlugins = [],
includeFallbackHtml = false,
projectRoot = process.cwd(),
exclude = []
}) {
const plugins = [
...vitePlugins,
...orgaBuildPlugin({
root,
outDir,
containerClass,
styles,
rehypePlugins,
exclude
})
]
if (includeFallbackHtml) {
// HTML fallback must be first so it can handle HTML navigation requests
// before runtime plugins (e.g. Cloudflare) potentially return 404.
plugins.unshift(htmlFallbackPlugin(projectRoot, styles))
}
return {
plugins,
resolve: { alias }
}
}
/**
* Checks if a user-provided index.html exists in the project root.
*
* @param {string} root - Project root directory
* @returns {Promise}
*/
async function hasUserIndexHtml(root) {
try {
await fs.access(path.join(root, 'index.html'), fs.constants.F_OK)
return true
} catch {
return false
}
}
/**
* Creates an HTML serving plugin that handles index.html for dev mode.
*
* This plugin performs per-request SSR in dev mode (matching Astro/SvelteKit behaviour):
* - SSR-renders each page on every request using Vite's server module runner
* - Injects rendered content and page metadata (%orga.*% placeholders) into the template
* - Falls back to the shell HTML for unknown routes (client-side router handles 404)
* - Only handles GET/HEAD requests that accept HTML
* - Does not intercept asset requests
*
* @param {string} projectRoot - Project root directory (where orga.config.js lives)
* @param {string[]} [styles]
* @returns {import('vite').Plugin}
*/
export function htmlFallbackPlugin(projectRoot, styles = []) {
return {
name: 'orga-build:html-fallback',
async configureServer(server) {
// Determine which index.html to use at startup
// Look for user's index.html in project root (where orga.config.js lives)
const userIndexPath = path.join(projectRoot, 'index.html')
const userHasIndex = await hasUserIndexHtml(projectRoot)
const indexHtmlPath = userHasIndex ? userIndexPath : defaultIndexHtml
// CJS compatibility here depends on Vite externalizing bare-specifier deps
// (e.g. react/*) to native Node import(). If aliases rewrite specifiers
// to absolute paths, modules can be inlined and evaluated without CJS globals.
const runner = createServerModuleRunner(server.environments.ssr)
server.middlewares.use(async (req, res, next) => {
// Only handle GET/HEAD requests
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next()
}
const url = req.url || '/'
const pathname = url.split('?')[0]
// Endpoint routes are handled first and bypass HTML fallback.
try {
const { endpoints } = await runner.import(ssrEntry)
const endpointModule = endpoints?.[pathname]
if (endpointModule) {
const ctx = {
url: new URL(url, `http://${req.headers.host || 'localhost'}`),
params: {},
mode: /** @type {'dev'} */ ('dev'),
route: { route: pathname }
}
const response = await resolveEndpointResponse(
endpointModule,
ctx,
req.method
)
res.statusCode = response.status
response.headers.forEach((headerValue, headerName) => {
res.setHeader(headerName, headerValue)
})
if (req.method === 'HEAD') {
res.end()
return
}
const bytes = Buffer.from(await response.arrayBuffer())
res.end(bytes)
return
}
} catch (e) {
next(e)
return
}
// Only handle browser-like navigation requests.
// Don't match generic */* accepts to avoid hijacking API requests.
const accept = req.headers.accept || ''
if (!accept.includes('text/html')) {
return next()
}
// Don't intercept asset requests (files with extensions)
if (pathname !== '/' && /\.\w+$/.test(pathname)) {
return next()
}
try {
// Import via the runner on each request — the module graph handles
// HMR invalidation so stale modules are never served.
const { render, pages } = await runner.import(ssrEntry)
const content = render(pathname)
let html = await fs.readFile(indexHtmlPath, 'utf-8')
html = await server.transformIndexHtml(url, html)
const uniqueCssUrls = [...new Set(styles)]
if (uniqueCssUrls.length > 0) {
const cssLinks = uniqueCssUrls
.map((u) => ` `)
.join('')
html = html.replace('', `${cssLinks}`)
}
if (content) {
const ssr = { routePath: pathname }
html = html.replace(
'
',
`${content}
`
)
}
// Replace %orga.*% placeholders with page metadata
const page = pages[pathname]
if (page) {
html = html.replace(/%orga\.(\w+)%/g, (_, key) => {
const value = page[key] ?? ''
return escapeHtml(String(value))
})
}
// Strip any remaining unresolved placeholders (unknown route)
html = html.replace(/%orga\.\w+%/g, '')
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end(html)
} catch (e) {
next(e)
}
})
}
}
}
================================================
FILE: packages/orga-build/lib/serve.js
================================================
import path from 'node:path'
import { createServer } from 'vite'
import { alias, createOrgaBuildConfig } from './plugin.js'
/**
* Start the development server using native Vite.
*
* @param {import('./config.js').Config} config
* @param {number} [port]
* @param {string} [projectRoot]
*/
export async function serve(config, port = 3000, projectRoot = process.cwd()) {
const { plugins } = createOrgaBuildConfig({
root: config.root,
outDir: config.outDir,
containerClass: config.containerClass,
styles: config.styles ?? [],
rehypePlugins: config.rehypePlugins ?? [],
vitePlugins: config.vitePlugins,
includeFallbackHtml: true,
projectRoot,
exclude: config.exclude ?? []
})
const server = await createServer({
plugins,
appType: 'custom',
// Aliases are scoped to the client environment only.
// The SSR environment must NOT have these aliases: they convert bare specifiers
// (e.g. 'react') into absolute paths, which bypasses Vite's fetchModule
// externalization branch and causes CJS packages to be evaluated inline by
// ESModulesEvaluator (which has no 'module'/'require' globals).
environments: {
client: {
resolve: /** @type {any} */ ({ alias })
}
},
server: {
port,
strictPort: false,
watch: {
ignored: [`${path.resolve(config.outDir)}/**`]
}
}
})
await server.listen()
server.printUrls()
}
================================================
FILE: packages/orga-build/lib/ssr.jsx
================================================
import { renderToString } from 'react-dom/server'
import { Router } from 'wouter'
import endpoints from '/@orga-build/endpoints'
import pages from '/@orga-build/pages'
import { App } from './app.jsx'
export { pages }
export { endpoints }
/**
* @param {string} url
*/
export function render(url) {
const page = pages[url]
if (!page) {
console.log(`no page found for ${url}`)
return
}
const ssrContext = {}
console.log(`rendering ${url}`)
return renderToString(
)
}
================================================
FILE: packages/orga-build/lib/util.js
================================================
import { exec } from 'node:child_process'
import { createElement } from 'react'
export function buildNav() {}
/**
* @param {string} file
* @param {...RegExp|string} patterns
* @returns {boolean}
*/
export function match(file, ...patterns) {
return patterns.some((p) => {
if (p instanceof RegExp) {
return p.test(file)
}
return file === p
})
}
/**
* Default layout
* @param {Object} props
* @param {string|undefined} props.title
* @param {import('react').ReactNode} props.children
* @returns {React.JSX.Element}
*/
export function DefaultLayout({ title, children }) {
return createElement(
'html',
{ lang: 'en' },
createElement(
'head',
{},
createElement('meta', { charSet: 'utf-8' }),
createElement('meta', {
name: 'viewport',
content: 'width=device-width, initial-scale=1'
}),
title && createElement('title', {}, title)
),
createElement('body', {}, children)
)
}
/**
* @param {string} str
* @returns {string}
*/
export function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>')
}
/**
* @param {string} cmd
*/
export async function $(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout, stderr) => {
if (err) {
reject(err)
}
if (stderr) {
console.error(stderr)
}
console.log(stdout)
resolve(stdout)
})
})
}
================================================
FILE: packages/orga-build/lib/vite.js
================================================
import path from 'node:path'
import { setup } from './files.js'
const magicModulePrefix = '/@orga-build/'
const pagesModuleId = `${magicModulePrefix}pages`
const endpointsModuleId = `${magicModulePrefix}endpoints`
export const appEntryId = `${magicModulePrefix}main.js`
const contentModuleId = 'orga-build:content'
const contentModuleIdResolved = `\0${contentModuleId}`
const endpointModulePrefix = `${endpointsModuleId}/__route__/`
/**
* @param {Object} options
* @param {string} options.dir
* @param {string} [options.outDir]
* @param {string[]} [options.styles]
* @param {string[]} [options.exclude]
* @returns {import('vite').Plugin}
*/
export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
const files = setup(dir, { outDir, exclude })
return {
name: 'vite-plugin-orga-pages',
enforce: 'pre',
config: (_config, _env) => ({
future: {
removePluginHookSsrArgument: 'warn',
removePluginHookHandleHotUpdate: 'warn',
removeSsrLoadModule: 'warn'
}
}),
async configureServer(_server) {
// Eagerly run file discovery so route conflicts surface at startup
await files.pages()
await files.endpoints()
},
hotUpdate() {
// Invalidate in-memory file caches so added/removed routes are picked up
files.invalidate()
// Invalidate content module when content files change
const module = this.environment.moduleGraph.getModuleById(
contentModuleIdResolved
)
if (module) {
this.environment.moduleGraph.invalidateModule(module)
// Full reload for now; can optimize to HMR later
this.environment.hot.send({ type: 'full-reload', path: '*' })
}
},
async resolveId(id, _importer) {
if (id === appEntryId) {
return appEntryId
}
if (id === contentModuleId) {
return contentModuleIdResolved
}
if (id.startsWith(magicModulePrefix)) {
return id
}
},
async load(id) {
if (id === appEntryId) {
const styleImports = styles
.map((styleUrl) => `import ${JSON.stringify(styleUrl)};`)
.join('\n')
return `${styleImports}\nimport "orga-build/csr";`
}
if (id === contentModuleIdResolved) {
return await renderContentModule()
}
if (id === pagesModuleId) {
return await renderPageList()
}
if (id === endpointsModuleId) {
return await renderEndpointList()
}
if (id.startsWith(pagesModuleId)) {
const pageId = id.replace(pagesModuleId, '')
const page = await files.page(pageId)
if (page) {
return `
export * from '${page.dataPath}';
export {default} from '${page.dataPath}';
`
}
}
if (id.startsWith(endpointModulePrefix)) {
const routeHex = id.slice(endpointModulePrefix.length)
const endpointId = Buffer.from(routeHex, 'hex').toString('utf-8')
const endpoint = await files.endpoint(endpointId)
if (endpoint) {
return `export * from '${endpoint.dataPath}';`
}
}
if (id === `${magicModulePrefix}layouts`) {
const layouts = await files.layouts()
/** @type {string[]} */
const imports = []
const lines = Object.entries(layouts).map(([key, value], i) => {
imports.push(`import Layout${i} from '${value}'`)
return `layouts['${key}'] = Layout${i}`
})
return `
${imports.join('\n')}
const layouts = {};
${lines.join('\n')}
export default layouts;
`
}
if (id === `${magicModulePrefix}components`) {
return await renderComponents()
}
}
}
async function renderPageList() {
const pages = await files.pages()
return renderModuleMap('pages', pages, (id) =>
path.join(magicModulePrefix, 'pages', id)
)
}
async function renderEndpointList() {
const endpoints = await files.endpoints()
return renderModuleMap(
'endpoints',
endpoints,
(route) => endpointModulePrefix + Buffer.from(route).toString('hex')
)
}
/**
* @param {string} name
* @param {Record} entries
* @param {(key: string) => string} toModulePath
*/
function renderModuleMap(name, entries, toModulePath) {
/** @type {string[]} */
const imports = []
/** @type {string[]} */
const assignments = []
Object.keys(entries).forEach((key, i) => {
imports.push(`import * as m${i} from '${toModulePath(key)}'`)
assignments.push(`${name}['${key}'] = m${i}`)
})
return [
imports.join('\n'),
`const ${name} = {};`,
assignments.join('\n'),
`export default ${name};`
].join('\n')
}
async function renderComponents() {
const components = await files.components()
if (components) {
return `export * from '${components}'`
}
return ''
}
async function renderContentModule() {
const entries = await files.contentEntries()
const manifest = JSON.stringify(entries, null, 2)
return `
const __entries = ${manifest}
function normalizePath(path = '') {
return String(path).replace(/^\\/+|\\/+$/g, '')
}
function pathMatches(entryPath, queryPath) {
const e = normalizePath(entryPath)
const q = normalizePath(queryPath)
if (!q) return true
return e === q || e.startsWith(q + '/')
}
export function getPages(path = '', filter) {
const list = __entries.filter((e) => pathMatches(e.path, path))
return typeof filter === 'function' ? list.filter(filter) : list
}
export function getPage(idOrSlug, path = '') {
return __entries.find((e) => {
if (!pathMatches(e.path, path)) return false
return e.id === idOrSlug || e.slug === idOrSlug
})
}
export function getEntries(refs) {
return refs.map((r) => getPage(r.id, r.path || ''))
}
export const getCollection = getPages
export const getEntry = getPage
`
}
}
================================================
FILE: packages/orga-build/lib/watch.js
================================================
import fs from 'node:fs/promises'
/**
* @param {import("fs").PathLike} dir
* @param {RegExp} ignore
* @param {{(event: fs.FileChangeInfo): Promise | void}} onChange
*/
export async function watch(dir, ignore, onChange) {
let busy = false
let dirty = false
/** @type {ReturnType | null} */
let timeout = null
const delay = 1000
const defaultIgnorePattern = /node_modules|\.git|\.DS_Store|\.orga-build/
const watcher = fs.watch(dir, { recursive: true })
for await (const event of watcher) {
if (
event.eventType !== 'change' ||
event.filename === null ||
shouldIgnore(event.filename)
) {
continue
}
console.log(`file changed: ${event.filename}`)
dirty = true
if (busy) {
continue
}
if (timeout !== null) clearTimeout(timeout)
timeout = setTimeout(processEvent, delay, event)
}
/**
* @param {any} event
*/
async function processEvent(event) {
busy = true
dirty = false
await onChange(event)
busy = false
if (dirty) {
processEvent(event)
}
}
/**
* @param {string} filename
*/
function shouldIgnore(filename) {
return defaultIgnorePattern.test(filename) || ignore.test(filename)
}
}
================================================
FILE: packages/orga-build/package.json
================================================
{
"name": "orga-build",
"version": "0.9.0",
"description": "A simple tool that builds org-mode files into a website",
"type": "module",
"engines": {
"node": ">=20.19.0"
},
"bin": {
"orga-build": "./cli.js"
},
"scripts": {
"clean": "fd . -e d.ts -e d.ts.map -I -x rm {}",
"test": "node --test --no-warnings \"lib/__tests__/*.test.js\""
},
"files": [
"lib/",
"cli.js",
"index.js",
"index.d.ts",
"index.d.ts.map",
"lib/content.d.ts"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
},
"./csr": "./lib/csr.jsx",
"./components": "./lib/components.js",
"./client": {
"types": "./lib/content.d.ts"
}
},
"keywords": [
"orgajs",
"org-mode",
"build",
"website",
"react"
],
"author": "Xiaoxing Hu ",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/orga-build"
},
"dependencies": {
"@orgajs/rollup": "workspace:*",
"@vitejs/plugin-react": "^5.1.4",
"globby": "^14.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rehype-katex": "^7.0.1",
"unified": "^11.0.5",
"unist-util-visit-parents": "^6.0.1",
"vite": "^7.3.1",
"wouter": "^3.7.0"
},
"devDependencies": {
"@types/hast": "^3.0.4",
"@types/node": "^25.3.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"orga": "workspace:*"
}
}
================================================
FILE: packages/orga-build/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/orgx/CHANGELOG.md
================================================
# @orgajs/orgx
## 2.6.1
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- @orgajs/reorg-parse@4.4.1
- @orgajs/reorg-rehype@4.3.9
## 2.6.0
### Minor Changes
- a53cfea: all about the editor
This release improves the editor with new fold/shift/todo actions and settings, while also refactoring orga tokenization/parsing and lezer conversion to improve TODO handling, context hashing, and tree generation consistency.
### Patch Changes
- Updated dependencies [a53cfea]
- @orgajs/reorg-parse@4.4.0
- @orgajs/reorg-rehype@4.3.3
## 2.5.2
### Patch Changes
- @orgajs/reorg-parse@4.3.1
- @orgajs/reorg-rehype@4.3.2
## 2.5.1
### Patch Changes
- @orgajs/reorg-rehype@4.3.1
## 2.5.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- @orgajs/reorg-rehype@4.3.0
- @orgajs/reorg-parse@4.3.0
## 2.4.1
### Patch Changes
- e3ef3a5: build website with orga-build
## 2.4.0
### Minor Changes
- 351f690: introduce @orgajs/node-loader, @orgajs/esbuild, @orgajs/build
- @orgajs/node-loader : the nodejs loader for org-mode files
- @orgajs/esbuild : esbuild plugin
- @orgajs/build : static site generator, a.k.a orga-build
## 2.3.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- @orgajs/reorg-rehype@4.2.0
- @orgajs/reorg-parse@4.2.0
## 2.2.2
### Patch Changes
- @orgajs/reorg-rehype@4.1.3
## 2.2.1
### Patch Changes
- @orgajs/reorg-parse@4.1.2
- @orgajs/reorg-rehype@4.1.2
## 2.2.0
### Minor Changes
- ac322714: implement editor
### Patch Changes
- @orgajs/reorg-parse@4.1.1
- @orgajs/reorg-rehype@4.1.1
## 2.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- @orgajs/reorg-rehype@4.1.0
- @orgajs/reorg-parse@4.1.0
## 2.0.1
### Patch Changes
- 1dbf674d: fix layout issue
`OrgaLayout` was renamed to `OrgLayout`.
## 2.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- @orgajs/reorg-rehype@4.0.0
- @orgajs/reorg-parse@4.0.0
## 1.0.8
### Patch Changes
- eeccc870: - get image links out of paragraph
- some other minor fixes
- Updated dependencies [eeccc870]
- @orgajs/reorg-rehype@3.0.10
- @orgajs/reorg-parse@3.1.7
## 1.0.7
### Patch Changes
- 6c1ddb9f: add latex support
- @orgajs/reorg-rehype@3.0.9
- @orgajs/reorg-parse@3.1.6
## 1.0.6
### Patch Changes
- 4bde5155: tidy up dependencies
- @orgajs/reorg-parse@3.1.5
- @orgajs/reorg-rehype@3.0.8
## 1.0.5
### Patch Changes
- @orgajs/reorg-rehype@3.0.7
- @orgajs/reorg-parse@3.1.4
## 1.0.4
### Patch Changes
- @orgajs/reorg-parse@3.1.3
- @orgajs/reorg-rehype@3.0.6
## 1.0.3
### Patch Changes
- cd7cac3d: export evaluateSync
## 1.0.2
### Patch Changes
- c8edd571: add evaluateSync function to orgx
## 1.0.1
### Patch Changes
- 594bf16b: ## @orgajs/orgx
Introducing new compiler `@orgajs/orgx`. It's a (almost) a direct port of [xdm](https://github.com/wooorm/xdm).
Most of the packages have already adopted `@orgajs/orgx`. The important ones are:
- `@orgajs/loader`
- `@orgajs/next`
- `gatsby-plugin-orga`
- `gatsby-theme-orga-docs`
- `@orgajs/playground'`
`gatsby-transformer-orga` is still using the original compiler, since it has it's own ecosystem which requires some work to do a proper migration. That means the derivative packages around it are using the original compiler.
- `gatsby-theme-orga-posts`
- `gatsby-theme-orga-posts-core`
## theme-ui support
`theme-ui` has `mdx` support builtin, and it's hard to do a clean extraction. So the package `@orgajs/theme-ui` is wrapping theme-ui, and provide orga specific tweaks. For gatsby, `gatsby-plugin-orga-theme-ui` is the equivalent of `gatsby-plugin-theme-ui`, but with orga support.
- @orgajs/reorg-parse@3.1.2
- @orgajs/reorg-rehype@3.0.5
================================================
FILE: packages/orgx/README.org
================================================
#+title: @orgajs/orgx
A (almost) direct port of [[https://github.com/wooorm/xdm][xdm]].
================================================
FILE: packages/orgx/index.js
================================================
/**
* @typedef {import('./lib/core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('./lib/compile.js').CompileOptions} CompileOptions
* @typedef {import('./lib/evaluate.js').EvaluateOptions} EvaluateOptions
* @typedef {import('./lib/types.js').OrgComponents} OrgComponents
* @typedef {import('./lib/types.js').OrgContent} OrgContent
* @typedef {import('./lib/types.js').OrgModule} OrgModule
* @typedef {import('./lib/types.js').OrgProps} OrgProps
*/
export { compile, compileSync } from './lib/compile.js'
export { createProcessor } from './lib/core.js'
export { evaluate, evaluateSync } from './lib/evaluate.js'
export { run, runSync } from './lib/run.js'
export { isOrgContent } from './lib/util/is-org-content.js'
================================================
FILE: packages/orgx/lib/compile.js
================================================
/**
* @typedef {import('vfile').VFile} VFile
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('./core.js').PluginOptions} PluginOptions
* @typedef {import('./core.js').BaseProcessorOptions} BaseProcessorOptions
*/
/**
* @typedef {Omit} CoreProcessorOptions
* Core configuration.
*
* @typedef ExtraOptions
* Extra configuration.
* @property {'detect' | 'mdx' | 'md' | null | undefined} [format='detect']
* Format of `file`.
*
* @typedef {CoreProcessorOptions & PluginOptions & ExtraOptions} CompileOptions
* Configuration.
*/
import { createProcessor } from './core.js'
import { resolveFileAndOptions } from './util/resolve-file-and-options.js'
/**
* Compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {CompileOptions | null | undefined} [compileOptions]
* Compile configuration.
* @return {Promise}
* File.
*/
export function compile(vfileCompatible, compileOptions) {
const { file, options } = resolveFileAndOptions(
vfileCompatible,
compileOptions
)
return createProcessor(options).process(file)
}
/**
* Synchronously compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {CompileOptions | null | undefined} [compileOptions]
* Compile configuration.
* @return {VFile}
* File.
*/
export function compileSync(vfileCompatible, compileOptions) {
const { file, options } = resolveFileAndOptions(
vfileCompatible,
compileOptions
)
return createProcessor(options).processSync(file)
}
================================================
FILE: packages/orgx/lib/core.js
================================================
/**
* @import {Program} from 'estree-jsx'
* @import {PluggableList, Processor} from 'unified'
* @typedef {import('./plugin/rehype-recma.js').Options} RehypeRecmaOptions
* @typedef {import('./plugin/recma-document.js').RecmaDocumentOptions} RecmaDocumentOptions
* @typedef {import('recma-stringify').Options} RecmaStringifyOptions
* @typedef {import('./plugin/recma-jsx-rewrite.js').RecmaJsxRewriteOptions} RecmaJsxRewriteOptions
*/
/**
* @typedef BaseProcessorOptions
* Base configuration.
* @property {boolean | null | undefined} [jsx=false]
* Whether to keep JSX.
* Format of the files to be processed.
* @property {'function-body' | 'program'} [outputFormat='program']
* Whether to compile to a whole program or a function body..
* @property {PluggableList | null | undefined} [recmaPlugins]
* List of recma (esast, JavaScript) plugins.
* @property {PluggableList | null | undefined} [reorgPlugins]
* List of remark (mdast, markdown) plugins.
* @property {PluggableList | null | undefined} [rehypePlugins]
* List of rehype (hast, HTML) plugins.
* @property {import('@orgajs/reorg-rehype').Options | null | undefined} [reorgRehypeOptions]
* Options to pass through to `reorg-rehype`.
*
* @typedef {Omit} PluginOptions
* Configuration for internal plugins.
*
* @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
* Configuration for processor.
*/
import reorgParse from '@orgajs/reorg-parse'
import reorgRehype from '@orgajs/reorg-rehype'
import recmaBuildJsx from 'recma-build-jsx'
import recmaJsx from 'recma-jsx'
import recmaStringify from 'recma-stringify'
import { unified } from 'unified'
import { recmaBuildJsxTransform } from './plugin/recma-build-jsx-transform.js'
import { recmaDocument } from './plugin/recma-document.js'
import { recmaJsxRewrite } from './plugin/recma-jsx-rewrite.js'
import rehypeRecma from './plugin/rehype-recma.js'
/**
* Pipeline to:
*
* 1. Parse org-mode
* 2. Transform through reorg (oast), rehype (hast), and recma (esast)
* 3. Serialize as JavaScript
*
* @param {Readonly | null | undefined} [options]
* Configuration.
* @return {Processor}
* Processor.
*/
export function createProcessor(options) {
const {
development,
jsx,
outputFormat,
providerImportSource,
recmaPlugins,
rehypePlugins,
reorgPlugins,
reorgRehypeOptions,
elementAttributeNameCase,
stylePropertyNameCase,
SourceMapGenerator,
...rest
} = options || {}
const dev = development ?? false
const pipeline = unified()
.use(reorgParse)
.use(reorgPlugins || [])
.use(reorgRehype, {
...reorgRehypeOptions,
allowDangerousHtml: true
})
.use(rehypePlugins || [])
.use(rehypeRecma, { elementAttributeNameCase, stylePropertyNameCase })
.use(recmaDocument, { ...rest, outputFormat })
.use(recmaJsxRewrite, {
development: dev,
providerImportSource,
outputFormat
})
if (!jsx) {
pipeline
.use(recmaBuildJsx, { development: dev, outputFormat })
.use(recmaBuildJsxTransform, { outputFormat })
}
pipeline
.use(recmaJsx)
.use(recmaStringify, { SourceMapGenerator })
.use(recmaPlugins || [])
// @ts-expect-error: TS doesn’t get the plugins we added with if-statements.
return pipeline
}
================================================
FILE: packages/orgx/lib/evaluate.js
================================================
/**
* @typedef {import('./types.js').OrgModule} ExportMap
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('./util/resolve-evaluate-options.js').EvaluateOptions} EvaluateOptions
*/
import { compile, compileSync } from './compile.js'
import { run, runSync } from './run.js'
import { resolveEvaluateOptions } from './util/resolve-evaluate-options.js'
/**
* Evaluate Org Content.
*
* @param {VFileCompatible} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {EvaluateOptions} evaluateOptions
* Configuration for evaluation.
* @return {Promise}
* Export map.
*/
export async function evaluate(vfileCompatible, evaluateOptions) {
const { compiletime, runtime } = resolveEvaluateOptions(evaluateOptions)
return run(await compile(vfileCompatible, compiletime), runtime)
}
/**
* Synchronously evaluate MDX.
*
* @param {VFileCompatible} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {EvaluateOptions} evaluateOptions
* Configuration for evaluation.
* @return {ExportMap}
* Export map.
*/
export function evaluateSync(vfileCompatible, evaluateOptions) {
const { compiletime, runtime } = resolveEvaluateOptions(evaluateOptions)
return runSync(compileSync(vfileCompatible, compiletime), runtime)
}
================================================
FILE: packages/orgx/lib/plugin/recma-build-jsx-transform.js
================================================
/**
* @import {Program} from 'estree-jsx'
*/
/**
* @typedef Options
* Configuration for internal plugin `recma-build-jsx-transform`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to keep the import of the automatic runtime or get it from
* `arguments[0]` instead (default: `'program'`).
*/
import { specifiersToDeclarations } from '../util/estree-util-specifiers-to-declarations.js'
import { toIdOrMemberExpression } from '../util/estree-util-to-id-or-member-expression.js'
/**
* Plugin to change the tree after compiling JSX away.
*
* @param {Readonly | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export function recmaBuildJsxTransform(options) {
/* c8 ignore next -- always given in `@mdx-js/mdx` */
const { outputFormat } = options || {}
/**
* @param {Program} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
// Remove the pragma comment that we injected ourselves as it is no longer
// needed.
// if (tree.comments) {
// tree.comments = tree.comments.filter(function (d) {
// return !d.data?._mdxIsPragmaComment
// })
// }
// When compiling to a function body, replace the import that was just
// generated, and get `jsx`, `jsxs`, and `Fragment` from `arguments[0]`
// instead.
if (outputFormat === 'function-body') {
let index = 0
// Skip directives: JS currently only has `use strict`, but Acorn allows
// arbitrary ones.
// Practically things like `use client` could be used?
while (index < tree.body.length) {
const child = tree.body[index]
if ('directive' in child && child.directive) {
index++
} else {
break
}
}
const declaration = tree.body[index]
if (
declaration &&
declaration.type === 'ImportDeclaration' &&
typeof declaration.source.value === 'string' &&
/\/jsx-(dev-)?runtime$/.test(declaration.source.value)
) {
tree.body[index] = {
type: 'VariableDeclaration',
kind: 'const',
declarations: specifiersToDeclarations(
declaration.specifiers,
toIdOrMemberExpression(['arguments', 0])
)
}
}
}
}
}
================================================
FILE: packages/orgx/lib/plugin/recma-document.js
================================================
/**
* @typedef {import('estree-jsx').Directive} Directive
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportExpression} ImportExpression
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').Literal} Literal
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').Property} Property
* @typedef {import('estree-jsx').SimpleLiteral} SimpleLiteral
* @typedef {import('estree-jsx').SpreadElement} SpreadElement
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*/
/**
* @typedef RecmaDocumentOptions
* Configuration for internal plugin `recma-document`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to use either `import` and `export` statements to get the runtime
* (and optionally provider) and export the content, or get values from
* `arguments` and return things.
* @property {boolean | null | undefined} [useDynamicImport=false]
* Whether to keep `import` (and `export … from`) statements or compile them
* to dynamic `import()` instead.
* @property {string | null | undefined} [baseUrl]
* Resolve `import`s (and `export … from`, and `import.meta.url`) relative to
* this URL.
* @property {string | null | undefined} [pragma='React.createElement']
* Pragma for JSX (used in classic runtime).
* @property {string | null | undefined} [pragmaFrag='React.Fragment']
* Pragma for JSX fragments (used in classic runtime).
* @property {string | null | undefined} [pragmaImportSource='react']
* Where to import the identifier of `pragma` from (used in classic runtime).
* @property {string | null | undefined} [jsxImportSource='react']
* Place to import automatic JSX runtimes from (used in automatic runtime).
* @property {'automatic' | 'classic' | null | undefined} [jsxRuntime='automatic']
* JSX runtime to use.
*/
import { walk } from 'estree-walker'
import { analyze } from 'periscopic'
import { positionFromEstree } from 'unist-util-position-from-estree'
import { stringifyPosition } from 'unist-util-stringify-position'
import { create } from '../util/estree-util-create.js'
import { declarationToExpression } from '../util/estree-util-declaration-to-expression.js'
import { isDeclaration } from '../util/estree-util-is-declaration.js'
import { specifiersToDeclarations } from '../util/estree-util-specifiers-to-declarations.js'
/**
* A plugin to wrap the estree in `OrgContent`.
*
* @type {import('unified').Plugin<[RecmaDocumentOptions | null | undefined] | [], Program>}
*/
export function recmaDocument(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
const options_ = options || {}
const baseUrl = options_.baseUrl || undefined
const useDynamicImport = options_.useDynamicImport || undefined
const outputFormat = options_.outputFormat || 'program'
const pragma =
options_.pragma === undefined ? 'React.createElement' : options_.pragma
const pragmaFrag =
options_.pragmaFrag === undefined ? 'React.Fragment' : options_.pragmaFrag
const pragmaImportSource = options_.pragmaImportSource || 'react'
const jsxImportSource = options_.jsxImportSource || 'react'
const jsxRuntime = options_.jsxRuntime || 'automatic'
return (tree, file) => {
/** @type {Array<[string, string] | string>} */
const exportedIdentifiers = []
/** @type {Array} */
const replacement = []
/** @type {Array} */
const pragmas = []
let exportAllCount = 0
/** @type {ExportDefaultDeclaration | ExportSpecifier | undefined} */
let layout
/** @type {boolean | undefined} */
let content
/** @type {Node} */
let child
// Patch missing comments, which types say could occur.
/* c8 ignore next */
if (!tree.comments) tree.comments = []
if (jsxRuntime) {
pragmas.push(`@jsxRuntime ${jsxRuntime}`)
}
if (jsxRuntime === 'automatic' && jsxImportSource) {
pragmas.push(`@jsxImportSource ${jsxImportSource}`)
}
if (jsxRuntime === 'classic' && pragma) {
pragmas.push(`@jsx ${pragma}`)
}
if (jsxRuntime === 'classic' && pragmaFrag) {
pragmas.push(`@jsxFrag ${pragmaFrag}`)
}
if (pragmas.length > 0) {
tree.comments.unshift({ type: 'Block', value: pragmas.join(' ') })
}
if (jsxRuntime === 'classic' && pragmaImportSource) {
if (!pragma) {
throw new Error(
'Missing `pragma` in classic runtime with `pragmaImportSource`'
)
}
handleEsm({
type: 'ImportDeclaration',
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: pragma.split('.')[0] }
}
],
source: { type: 'Literal', value: pragmaImportSource }
})
}
// Find the `export default`, the JSX expression, and leave the rest
// (import/exports) as they are.
for (child of tree.body) {
// ```js
// export default props => <>{props.children}>
// ```
//
// Treat it as an inline layout declaration.
if (child.type === 'ExportDefaultDeclaration') {
if (layout) {
file.fail(
'Cannot specify multiple layouts (previous: ' +
stringifyPosition(positionFromEstree(layout)) +
')',
positionFromEstree(child),
'recma-document:duplicate-layout'
)
}
layout = child
replacement.push({
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'OrgLayout' },
// @ts-expect-error
init: isDeclaration(child.declaration)
? declarationToExpression(child.declaration)
: child.declaration
}
]
})
}
// ```js
// export {a, b as c} from 'd'
// ```
else if (child.type === 'ExportNamedDeclaration' && child.source) {
const source = /** @type {SimpleLiteral} */ (child.source)
// Remove `default` or `as default`, but not `default as`, specifier.
child.specifiers = child.specifiers.filter((specifier) => {
// @ts-expect-error
if (specifier.exported.name === 'default') {
if (layout) {
file.fail(
'Cannot specify multiple layouts (previous: ' +
stringifyPosition(positionFromEstree(layout)) +
')',
positionFromEstree(child),
'recma-document:duplicate-layout'
)
}
layout = specifier
// Make it just an import: `import OrgLayout from '…'`.
/** @type {Array} */
const specifiers = []
// Default as default / something else as default.
// @ts-expect-error
if (specifier.local.name === 'default') {
specifiers.push({
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: 'OrgLayout' }
})
} else {
/** @type {ImportSpecifier} */
const importSpecifier = {
type: 'ImportSpecifier',
imported: specifier.local,
local: { type: 'Identifier', name: 'OrgLayout' }
}
create(specifier.local, importSpecifier)
specifiers.push(importSpecifier)
}
/** @type {Literal} */
const from = { type: 'Literal', value: source.value }
create(source, from)
/** @type {ImportDeclaration} */
const declaration = {
type: 'ImportDeclaration',
specifiers,
source: from
}
create(specifier, declaration)
handleEsm(declaration)
return false
}
return true
})
// If there are other things imported, keep it.
if (child.specifiers.length > 0) {
handleExport(child)
}
}
// ```js
// export {a, b as c}
// export * from 'a'
// ```
else if (
child.type === 'ExportNamedDeclaration' ||
child.type === 'ExportAllDeclaration'
) {
handleExport(child)
} else if (child.type === 'ImportDeclaration') {
handleEsm(child)
} else if (
child.type === 'ExpressionStatement' &&
(child.expression.type === 'JSXFragment' ||
child.expression.type === 'JSXElement')
) {
content = true
replacement.push(...createMdxContent(child.expression, Boolean(layout)))
// The following catch-all branch is because plugins might’ve added
// other things.
// Normally, we only have import/export/jsx, but just add whatever’s
// there.
/* c8 ignore next 3 */
} else {
replacement.push(child)
}
}
// If there was no JSX content at all, add an empty function.
if (!content) {
replacement.push(...createMdxContent(undefined, Boolean(layout)))
}
exportedIdentifiers.push(['OrgContent', 'default'])
if (outputFormat === 'function-body') {
replacement.push({
type: 'ReturnStatement',
argument: {
type: 'ObjectExpression',
properties: [
...Array.from({ length: exportAllCount }).map(
/**
* @param {undefined} _
* @param {number} index
* @returns {SpreadElement}
*/
(_, index) => ({
type: 'SpreadElement',
argument: {
type: 'Identifier',
name: `_exportAll${index + 1}`
}
})
),
...exportedIdentifiers.map((d) => {
/** @type {Property} */
const prop = {
type: 'Property',
kind: 'init',
method: false,
computed: false,
shorthand: typeof d === 'string',
key: {
type: 'Identifier',
name: typeof d === 'string' ? d : d[1]
},
value: {
type: 'Identifier',
name: typeof d === 'string' ? d : d[0]
}
}
return prop
})
]
}
})
} else {
replacement.push({
type: 'ExportDefaultDeclaration',
declaration: { type: 'Identifier', name: 'OrgContent' }
})
}
tree.body = replacement
if (baseUrl) {
walk(tree, {
enter(node) {
if (
node.type === 'MemberExpression' &&
'object' in node &&
node.object.type === 'MetaProperty' &&
node.property.type === 'Identifier' &&
node.object.meta.name === 'import' &&
node.object.property.name === 'meta' &&
node.property.name === 'url'
) {
/** @type {SimpleLiteral} */
const replacement = { type: 'Literal', value: baseUrl }
this.replace(replacement)
}
}
})
}
/**
* @param {ExportAllDeclaration | ExportNamedDeclaration} node
* @returns {void}
*/
function handleExport(node) {
if (node.type === 'ExportNamedDeclaration') {
// ```js
// export function a() {}
// export class A {}
// export var a = 1
// ```
if (node.declaration) {
exportedIdentifiers.push(
...analyze(node.declaration).scope.declarations.keys()
)
}
// ```js
// export {a, b as c}
// export {a, b as c} from 'd'
// ```
for (child of node.specifiers) {
// @ts-expect-error
exportedIdentifiers.push(child.exported.name)
}
}
handleEsm(node)
}
/**
* @param {ExportAllDeclaration | ExportNamedDeclaration | ImportDeclaration} node
* @returns {void}
*/
function handleEsm(node) {
// Rewrite the source of the `import` / `export … from`.
// See:
if (baseUrl && node.source) {
let value = String(node.source.value)
try {
// A full valid URL.
value = String(new URL(value))
} catch {
// Relative: `/example.js`, `./example.js`, and `../example.js`.
if (/^\.{0,2}\//.test(value)) {
value = String(new URL(value, baseUrl))
}
// Otherwise, it’s a bare specifiers.
// For example `some-package`, `@some-package`, and
// `some-package/path`.
// These are supported in Node and browsers plan to support them
// with import maps ().
}
/** @type {Literal} */
const literal = { type: 'Literal', value }
create(node.source, literal)
node.source = literal
}
/** @type {ModuleDeclaration | Statement | undefined} */
let replace
/** @type {Expression} */
let init
if (outputFormat === 'function-body') {
if (
// Always have a source:
node.type === 'ImportDeclaration' ||
node.type === 'ExportAllDeclaration' ||
// Source optional:
(node.type === 'ExportNamedDeclaration' && node.source)
) {
if (!useDynamicImport) {
file.fail(
'Cannot use `import` or `export … from` in `evaluate` (outputting a function body) by default: please set `useDynamicImport: true` (and probably specify a `baseUrl`)',
positionFromEstree(node),
'recma-document:invalid-esm-statement'
)
}
// Just for types.
/* c8 ignore next 3 */
if (!node.source) {
throw new Error('Expected `node.source` to be defined')
}
// ```
// import 'a'
// //=> await import('a')
// import a from 'b'
// //=> const {default: a} = await import('b')
// export {a, b as c} from 'd'
// //=> const {a, c: b} = await import('d')
// export * from 'a'
// //=> const _exportAll0 = await import('a')
// ```
/** @type {ImportExpression} */
const argument = { type: 'ImportExpression', source: node.source }
create(node, argument)
init = { type: 'AwaitExpression', argument }
if (
(node.type === 'ImportDeclaration' ||
node.type === 'ExportNamedDeclaration') &&
node.specifiers.length === 0
) {
replace = { type: 'ExpressionStatement', expression: init }
} else {
replace = {
type: 'VariableDeclaration',
kind: 'const',
declarations:
node.type === 'ExportAllDeclaration'
? [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: `_exportAll${++exportAllCount}`
},
init
}
]
: specifiersToDeclarations(node.specifiers, init)
}
}
} else if (node.declaration) {
replace = node.declaration
} else {
/** @type {Array} */
// @ts-expect-error
const declarators = node.specifiers
.filter(
// @ts-expect-error
(specifier) => specifier.local.name !== specifier.exported.name
)
.map((specifier) => ({
type: 'VariableDeclarator',
id: specifier.exported,
init: specifier.local
}))
if (declarators.length > 0) {
replace = {
type: 'VariableDeclaration',
kind: 'const',
declarations: declarators
}
}
}
} else {
replace = node
}
if (replace) {
replacement.push(replace)
}
}
}
/**
* @param {Expression | undefined} [content]
* @param {boolean | undefined} [hasInternalLayout]
* @returns {Array}
*/
function createMdxContent(content, hasInternalLayout) {
/** @type {JSXElement} */
const element = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: { type: 'JSXIdentifier', name: 'OrgLayout' },
attributes: [
{
type: 'JSXSpreadAttribute',
argument: { type: 'Identifier', name: 'props' }
}
],
selfClosing: false
},
closingElement: {
type: 'JSXClosingElement',
name: { type: 'JSXIdentifier', name: 'OrgLayout' }
},
children: [
{
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: { type: 'JSXIdentifier', name: '_createOrgContent' },
attributes: [
{
type: 'JSXSpreadAttribute',
argument: { type: 'Identifier', name: 'props' }
}
],
selfClosing: true
},
closingElement: null,
children: []
}
]
}
let result = /** @type {Expression} */ (element)
if (!hasInternalLayout) {
result = {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'OrgLayout' },
consequent: result,
alternate: {
type: 'CallExpression',
callee: { type: 'Identifier', name: '_createOrgContent' },
arguments: [{ type: 'Identifier', name: 'props' }],
optional: false
}
}
}
let argument = content || { type: 'Literal', value: null }
// Unwrap a fragment of a single element.
if (
argument &&
argument.type === 'JSXFragment' &&
argument.children.length === 1 &&
argument.children[0].type === 'JSXElement'
) {
argument = argument.children[0]
}
return [
{
type: 'FunctionDeclaration',
id: { type: 'Identifier', name: '_createOrgContent' },
params: [{ type: 'Identifier', name: 'props' }],
body: {
type: 'BlockStatement',
body: [{ type: 'ReturnStatement', argument }]
}
},
{
type: 'FunctionDeclaration',
id: { type: 'Identifier', name: 'OrgContent' },
params: [
{
type: 'AssignmentPattern',
left: { type: 'Identifier', name: 'props' },
right: { type: 'ObjectExpression', properties: [] }
}
],
body: {
type: 'BlockStatement',
body: [{ type: 'ReturnStatement', argument: result }]
}
}
]
}
}
================================================
FILE: packages/orgx/lib/plugin/recma-jsx-rewrite.js
================================================
/**
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').Function} EstreeFunction
* @typedef {import('estree-jsx').Identifier} Identifier
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').ObjectPattern} ObjectPattern
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').Property} Property
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*
* @typedef {import('periscopic').Scope & {node: Node}} Scope
*/
/**
* @typedef RecmaJsxRewriteOptions
* Configuration for internal plugin `recma-jsx-rewrite`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to use an import statement or `arguments[0]` to get the provider.
* @property {string | null | undefined} [providerImportSource]
* Place to import a provider from.
* @property {boolean | null | undefined} [development=false]
* Whether to add extra info to error messages in generated code.
*
* This also results in the development automatic JSX runtime
* (`/jsx-dev-runtime`, `jsxDEV`) being used, which passes positional info to
* nodes.
* The default can be set to `true` in Node.js through environment variables:
* set `NODE_ENV=development`.
*
* @typedef StackEntry
* @property {Array} objects
* @property {Array} components
* @property {Array} tags
* @property {Record} references
* @property {Map} idToInvalidComponentName
* @property {EstreeFunction} node
*/
import { name as isIdentifierName } from 'estree-util-is-identifier-name'
import { walk } from 'estree-walker'
import { analyze } from 'periscopic'
import { positionFromEstree } from 'unist-util-position-from-estree'
import { stringifyPosition } from 'unist-util-stringify-position'
import { specifiersToDeclarations } from '../util/estree-util-specifiers-to-declarations.js'
import { toBinaryAddition } from '../util/estree-util-to-binary-addition.js'
import {
toIdOrMemberExpression,
toJsxIdOrMemberExpression
} from '../util/estree-util-to-id-or-member-expression.js'
const own = {}.hasOwnProperty
/**
* A plugin that rewrites JSX in functions to accept components as
* `props.components` (when the function is called `_createOrgContent`), or from
* a provider (if there is one).
* It also makes sure that any undefined components are defined: either from
* received components or as a function that throws an error.
*
* @type {import('unified').Plugin<[RecmaJsxRewriteOptions | null | undefined] | [], Program>}
*/
export function recmaJsxRewrite(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
const { development, providerImportSource, outputFormat } = options || {}
return (tree, file) => {
// Find everything that’s defined in the top-level scope.
const scopeInfo = analyze(tree)
/** @type {Array} */
const fnStack = []
let importProvider = false
let createErrorHelper = false
/** @type {Scope | undefined} */
let currentScope
walk(tree, {
enter(node) {
const newScope = /** @type {Scope | undefined} */ (
scopeInfo.map.get(node)
)
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
fnStack.push({
objects: [],
components: [],
tags: [],
references: {},
idToInvalidComponentName: new Map(),
node
})
// OrgContent only ever contains OrgLayout
if (
isNamedFunction(node, 'OrgContent') &&
newScope &&
!inScope(newScope, 'OrgLayout')
) {
fnStack[0].components.push('OrgLayout')
}
}
const fnScope = fnStack[0]
if (
!fnScope ||
(!isNamedFunction(fnScope.node, '_createOrgContent') &&
!providerImportSource)
) {
return
}
if (newScope) {
newScope.node = node
currentScope = newScope
}
if (currentScope && node.type === 'JSXElement') {
let name = node.openingElement.name
// ``, ``, ``.
if (name.type === 'JSXMemberExpression') {
/** @type {Array} */
const ids = []
// Find the left-most identifier.
while (name.type === 'JSXMemberExpression') {
ids.unshift(name.property.name)
name = name.object
}
ids.unshift(name.name)
const fullId = ids.join('.')
const id = name.name
const isInScope = inScope(currentScope, id)
if (!own.call(fnScope.references, fullId)) {
const parentScope = /** @type {Scope | undefined} */ (
currentScope.parent
)
if (
!isInScope ||
// If the parent scope is `_createOrgContent`, then this
// references a component we can add a check statement for.
(parentScope &&
parentScope.node.type === 'FunctionDeclaration' &&
isNamedFunction(parentScope.node, '_createOrgContent'))
) {
fnScope.references[fullId] = { node, component: true }
}
}
if (!fnScope.objects.includes(id) && !isInScope) {
fnScope.objects.push(id)
}
}
// ``.
else if (name.type === 'JSXNamespacedName') {
// Ignore namespaces.
}
// If the name is a valid ES identifier, and it doesn’t start with a
// lowercase letter, it’s a component.
// For example, `$foo`, `_bar`, `Baz` are all component names.
// But `foo` and `b-ar` are tag names.
else if (isIdentifierName(name.name) && !/^[a-z]/.test(name.name)) {
const id = name.name
if (!inScope(currentScope, id)) {
// No need to add an error for an undefined layout — we use an
// `if` later.
if (id !== 'OrgLayout' && !own.call(fnScope.references, id)) {
fnScope.references[id] = { node, component: true }
}
if (!fnScope.components.includes(id)) {
fnScope.components.push(id)
}
}
}
// @ts-expect-error Allow fields passed through from mdast through hast to
// esast.
else if (node.data?._mdxExplicitJsx) {
// Do not turn explicit JSX into components from `_components`.
// As in, a given `h1` component is used for `# heading` (next case),
// but not for `heading `.
} else {
const id = name.name
if (!fnScope.tags.includes(id)) {
fnScope.tags.push(id)
}
/** @type {Array} */
let jsxIdExpression = ['_components', id]
if (isIdentifierName(id) === false) {
let invalidComponentName =
fnScope.idToInvalidComponentName.get(id)
if (invalidComponentName === undefined) {
invalidComponentName = `_component${fnScope.idToInvalidComponentName.size}`
fnScope.idToInvalidComponentName.set(id, invalidComponentName)
}
jsxIdExpression = [invalidComponentName]
}
node.openingElement.name =
toJsxIdOrMemberExpression(jsxIdExpression)
if (node.closingElement) {
node.closingElement.name =
toJsxIdOrMemberExpression(jsxIdExpression)
}
}
}
},
leave(node) {
/** @type {Array} */
const defaults = []
/** @type {Array} */
const actual = []
/** @type {Array} */
const parameters = []
/** @type {Array} */
const declarations = []
if (currentScope && currentScope.node === node) {
// @ts-expect-error: `node`s were patched when entering.
currentScope = currentScope.parent
}
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
const fn = node
const scope = fnStack[fnStack.length - 1]
/** @type {string} */
let name
for (name of scope.tags) {
defaults.push({
type: 'Property',
kind: 'init',
key: isIdentifierName(name)
? { type: 'Identifier', name }
: { type: 'Literal', value: name },
value: { type: 'Literal', value: name },
method: false,
shorthand: false,
computed: false
})
}
actual.push(...scope.components)
for (name of scope.objects) {
// In some cases, a component is used directly (``) but it’s also
// used as an object (``).
if (!actual.includes(name)) {
actual.push(name)
}
}
/** @type {Array} */
const statements = []
if (
defaults.length > 0 ||
actual.length > 0 ||
scope.idToInvalidComponentName.size > 0
) {
if (providerImportSource) {
importProvider = true
parameters.push({
type: 'CallExpression',
callee: { type: 'Identifier', name: '_provideComponents' },
arguments: [],
optional: false
})
}
// Accept `components` as a prop if this is the `OrgContent` or
// `_createOrgContent` function.
if (
isNamedFunction(scope.node, 'OrgContent') ||
isNamedFunction(scope.node, '_createOrgContent')
) {
parameters.push(toIdOrMemberExpression(['props', 'components']))
}
if (defaults.length > 0 || parameters.length > 1) {
parameters.unshift({
type: 'ObjectExpression',
properties: defaults
})
}
// If we’re getting components from several sources, merge them.
/** @type {Expression} */
let componentsInit =
parameters.length > 1
? {
type: 'CallExpression',
callee: toIdOrMemberExpression(['Object', 'assign']),
arguments: parameters,
optional: false
}
: parameters[0].type === 'MemberExpression'
? // If we’re only getting components from `props.components`,
// make sure it’s defined.
{
type: 'LogicalExpression',
operator: '||',
left: parameters[0],
right: { type: 'ObjectExpression', properties: [] }
}
: parameters[0]
/** @type {ObjectPattern | undefined} */
let componentsPattern
// Add components to scope.
// For `['MyComponent', 'OrgLayout']` this generates:
// ```js
// const {MyComponent, wrapper: OrgLayout} = _components
// ```
// Note that OrgLayout is special as it’s taken from
// `_components.wrapper`.
if (actual.length > 0) {
componentsPattern = {
type: 'ObjectPattern',
properties: actual.map((name) => ({
type: 'Property',
kind: 'init',
key: {
type: 'Identifier',
name: name === 'OrgLayout' ? 'wrapper' : name
},
value: { type: 'Identifier', name },
method: false,
shorthand: name !== 'OrgLayout',
computed: false
}))
}
}
if (scope.tags.length > 0) {
declarations.push({
type: 'VariableDeclarator',
id: { type: 'Identifier', name: '_components' },
init: componentsInit
})
componentsInit = { type: 'Identifier', name: '_components' }
}
if (isNamedFunction(scope.node, '_createOrgContent')) {
for (const [
id,
componentName
] of scope.idToInvalidComponentName) {
// For JSX IDs that can’t be represented as JavaScript IDs (as in,
// those with dashes, such as `custom-element`), generate a
// separate variable that is a valid JS ID (such as `_component0`),
// and takes it from components:
// `const _component0 = _components['custom-element']`
declarations.push({
type: 'VariableDeclarator',
id: { type: 'Identifier', name: componentName },
init: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_components' },
property: { type: 'Literal', value: id },
computed: true,
optional: false
}
})
}
}
if (componentsPattern) {
declarations.push({
type: 'VariableDeclarator',
id: componentsPattern,
init: componentsInit
})
}
if (declarations.length > 0) {
statements.push({
type: 'VariableDeclaration',
kind: 'const',
declarations
})
}
}
/** @type {string} */
let key
// Add partials (so for `x.y.z` it’d generate `x` and `x.y` too).
for (key in scope.references) {
if (own.call(scope.references, key)) {
const parts = key.split('.')
let index = 0
while (++index < parts.length) {
const partial = parts.slice(0, index).join('.')
if (!own.call(scope.references, partial)) {
scope.references[partial] = {
node: scope.references[key].node,
component: false
}
}
}
}
}
const references = Object.keys(scope.references).sort()
let index = -1
while (++index < references.length) {
const id = references[index]
const info = scope.references[id]
const place = stringifyPosition(positionFromEstree(info.node))
/** @type {Array} */
const parameters = [
{ type: 'Literal', value: id },
{ type: 'Literal', value: info.component }
]
createErrorHelper = true
if (development && place !== '1:1-1:1') {
parameters.push({ type: 'Literal', value: place })
}
statements.push({
type: 'IfStatement',
test: {
type: 'UnaryExpression',
operator: '!',
prefix: true,
argument: toIdOrMemberExpression(id.split('.'))
},
consequent: {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: '_missingMdxReference' },
arguments: parameters,
optional: false
}
},
alternate: null
})
}
if (statements.length > 0) {
// Arrow functions with an implied return:
if (fn.body.type !== 'BlockStatement') {
fn.body = {
type: 'BlockStatement',
body: [{ type: 'ReturnStatement', argument: fn.body }]
}
}
fn.body.body.unshift(...statements)
}
fnStack.pop()
}
}
})
// If a provider is used (and can be used), import it.
if (importProvider && providerImportSource) {
tree.body.unshift(
createImportProvider(providerImportSource, outputFormat)
)
}
// If potentially missing components are used.
if (createErrorHelper) {
/** @type {Array} */
const message = [
{ type: 'Literal', value: 'Expected ' },
{
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'component' },
consequent: { type: 'Literal', value: 'component' },
alternate: { type: 'Literal', value: 'object' }
},
{ type: 'Literal', value: ' `' },
{ type: 'Identifier', name: 'id' },
{
type: 'Literal',
value:
'` to be defined: you likely forgot to import, pass, or provide it.'
}
]
/** @type {Array} */
const parameters = [
{ type: 'Identifier', name: 'id' },
{ type: 'Identifier', name: 'component' }
]
if (development) {
message.push({
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'place' },
consequent: toBinaryAddition([
{ type: 'Literal', value: '\nIt’s referenced in your code at `' },
{ type: 'Identifier', name: 'place' },
{
type: 'Literal',
value: `${file.path ? `\` in \`${file.path}` : ''}\``
}
]),
alternate: { type: 'Literal', value: '' }
})
parameters.push({ type: 'Identifier', name: 'place' })
}
tree.body.push({
type: 'FunctionDeclaration',
id: { type: 'Identifier', name: '_missingMdxReference' },
generator: false,
async: false,
params: parameters,
body: {
type: 'BlockStatement',
body: [
{
type: 'ThrowStatement',
argument: {
type: 'NewExpression',
callee: { type: 'Identifier', name: 'Error' },
arguments: [toBinaryAddition(message)]
}
}
]
}
})
}
}
}
/**
* @param {string} providerImportSource
* @param {RecmaJsxRewriteOptions['outputFormat']} outputFormat
* @returns {Statement | ModuleDeclaration}
*/
function createImportProvider(providerImportSource, outputFormat) {
/** @type {Array} */
const specifiers = [
{
type: 'ImportSpecifier',
imported: { type: 'Identifier', name: 'useOrgComponents' },
local: { type: 'Identifier', name: '_provideComponents' }
}
]
return outputFormat === 'function-body'
? {
type: 'VariableDeclaration',
kind: 'const',
declarations: specifiersToDeclarations(
specifiers,
toIdOrMemberExpression(['arguments', 0])
)
}
: {
type: 'ImportDeclaration',
specifiers,
source: { type: 'Literal', value: providerImportSource }
}
}
/**
* @param {EstreeFunction} node
* @param {string} name
* @returns {boolean}
*/
function isNamedFunction(node, name) {
return Boolean(node && 'id' in node && node.id && node.id.name === name)
}
/**
* @param {Scope} scope
* @param {string} id
* @returns {boolean}
*/
function inScope(scope, id) {
/** @type {Scope | undefined} */
let currentScope = scope
while (currentScope) {
if (currentScope.declarations.has(id)) {
return true
}
// @ts-expect-error: `node`s have been added when entering.
currentScope = currentScope.parent
}
return false
}
================================================
FILE: packages/orgx/lib/plugin/rehype-recma.js
================================================
/**
* @import {Root} from 'hast'
* @import {Program, Expression, ModuleDeclaration} from 'estree'
* @typedef {import('hast-util-to-estree').Options} Options
*/
import { Parser } from 'acorn'
import jsx from 'acorn-jsx'
import { toEstree } from 'hast-util-to-estree'
import renderError from '../util/render-error.js'
/**
* Plugin to transform HTML (hast) to JS (estree).
*
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export default function rehypeRecma(options) {
/**
* @param {Root} tree
* Tree (hast).
* @returns {Program}
* Program (esast).
*/
return function (tree) {
const data = tree.data || {}
/** @type {ModuleDeclaration[]} */
const prepend = []
Object.entries(data).forEach(([k, v]) => {
if (k === 'layout') {
prepend.push({
type: 'ImportDeclaration',
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: 'OrgLayout'
}
}
],
source: {
type: 'Literal',
value: `${v}`,
raw: `'${v}'`
}
})
} else {
prepend.push(createExport(k, v))
}
})
const estree = toEstree(tree, { ...options, handlers: { jsx: handleJsx } })
estree.body.unshift(...prepend)
return estree
}
}
const jsxParser = Parser.extend(jsx())
/**
* @param {import('acorn').Node | Program} node
* @returns {node is Program}
*/
function isProgram(node) {
return node.type === 'Program'
}
/**
* @param {string} code
*/
function parse(code) {
try {
return jsxParser.parse(code, {
sourceType: 'module',
ecmaVersion: 2020
})
} catch (err) {
// @ts-expect-error
return renderError(err)
}
}
/** @type {import("hast-util-to-estree").Handle} */
export const handleJsx = (node, state) => {
const skipImport = false
const estree = parse(node.value)
/** @type {Expression[]} */
const expressions = []
if (isProgram(estree)) {
estree.body.forEach((child) => {
if (child.type === 'ImportDeclaration') {
if (!skipImport) {
state.esm.push(child)
}
} else if (child.type === 'ExpressionStatement') {
expressions.push(child.expression)
} else if (
child.type === 'ExportDefaultDeclaration' ||
child.type === 'ExportNamedDeclaration'
) {
state.esm.push(child)
} else {
throw new Error(`unexpected node: ${child.type}`)
}
})
}
// @ts-expect-error: array works
// https://github.com/syntax-tree/hast-util-to-estree/blob/c0c4bd33583abade25c4f4e248a06cb1ec8c3aff/lib/state.js#L215
return expressions
}
/**
* @param {string} text
* @returns {string}
*/
function removeQuotes(text) {
return text.trim().replace(/^["'](.+(?=["']$))["']$/, '$1')
}
/**
* @param {string} k
* @param {any} v
* @returns {ModuleDeclaration}
*/
function createExport(k, v) {
/** @type {(text: string) => Expression} */
const createLiteral = (text) => {
const value = removeQuotes(`${text}`)
return { type: 'Literal', value, raw: `'${value}'` }
}
/** @type {Expression} */
const init = Array.isArray(v)
? {
type: 'ArrayExpression',
elements: v.map(createLiteral)
}
: createLiteral(v)
return {
type: 'ExportNamedDeclaration',
declaration: {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: k },
init
}
],
kind: 'const'
},
specifiers: [],
source: null
}
}
================================================
FILE: packages/orgx/lib/run.js
================================================
/** @type {new (code: string, ...args: Array) => Function} **/
const AsyncFunction = Object.getPrototypeOf(run).constructor
/**
* Asynchronously run code.
*
* @param {{toString(): string}} file
* JS document to run.
* @param {unknown} options
* Parameter.
* @return {Promise}
* Anything.
*/
export async function run(file, options) {
return new AsyncFunction(String(file))(options)
}
/**
* Synchronously run code.
*
* @param {{toString(): string}} file
* JS document to run.
* @param {unknown} options
* Parameter.
* @return {any}
* Anything.
*/
export function runSync(file, options) {
// eslint-disable-next-line no-new-func
return new Function(String(file))(options)
}
================================================
FILE: packages/orgx/lib/types.ts
================================================
type FunctionComponent = (props: Props) => React.JSX.Element | null
type ClassComponent = new (props: Props) => React.JSX.ElementClass
type Component =
| FunctionComponent
| ClassComponent
| keyof React.JSX.IntrinsicElements
interface NestedOrgComponents {
[key: string]: NestedOrgComponents | Component
}
export type OrgComponents = NestedOrgComponents & {
[Key in keyof React.JSX.IntrinsicElements]?: Component<
React.JSX.IntrinsicElements[Key]
>
} & {
/**
* If a wrapper component is defined, the org content will be wrapped inside of it.
*/
wrapper?: Component
}
export interface OrgProps {
/**
* Which props exactly may be passed into the component depends on the contents of the org
* file.
*/
[key: string]: unknown
/**
* This prop may be used to customize how certain components are rendered.
*/
components?: OrgComponents
}
export type OrgContent = (props: OrgProps) => React.JSX.Element
export interface OrgModule {
/**
* This could be any value that is exported from the org file.
*/
[key: string]: unknown
/**
* A functional React.JSX component which renders the content of the org file.
*/
default: OrgContent
}
================================================
FILE: packages/orgx/lib/util/estree-util-create.js
================================================
/**
* @typedef {import('estree-jsx').Node} Node
*/
/**
* @param {Node} from
* Node to take from.
* @param {Node} to
* Node to add to.
* @returns {void}
* Nothing.
*/
export function create(from, to) {
/** @type {Array} */
// @ts-expect-error: `start`, `end`, `comments` are custom Acorn fields.
const fields = ['start', 'end', 'loc', 'range', 'comments']
let index = -1
while (++index < fields.length) {
const field = fields[index]
if (field in from) {
// @ts-expect-error: assume they’re settable.
to[field] = from[field]
}
}
}
================================================
FILE: packages/orgx/lib/util/estree-util-declaration-to-expression.js
================================================
/**
* @typedef {import('estree-jsx').Declaration} Declaration
* @typedef {import('estree-jsx').Expression} Expression
*/
/**
* Turn a declaration into an expression.
*
* Doesn’t work for variable declarations, but that’s fine for our use case
* because currently we’re using this utility for export default declarations,
* which can’t contain variable declarations.
*
* @param {Declaration} declaration
* Declaration.
* @returns {Expression}
* Expression.
*/
export function declarationToExpression(declaration) {
if (declaration.type === 'FunctionDeclaration') {
return { ...declaration, type: 'FunctionExpression' }
}
if (declaration.type === 'ClassDeclaration') {
return { ...declaration, type: 'ClassExpression' }
/* Internal utility so the next shouldn’t happen or a maintainer is making a
* mistake. */
/* c8 ignore next 4 */
}
// Probably `VariableDeclaration`.
throw new Error(`Cannot turn \`${declaration.type}\` into an expression`)
}
================================================
FILE: packages/orgx/lib/util/estree-util-is-declaration.js
================================================
/**
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Declaration} Declaration
*/
/**
* Check if `node` is a declaration.
*
* @param {Node} node
* Node to check.
* @returns {node is Declaration}
* Whether `node` is a declaration.
*/
export function isDeclaration(node) {
return Boolean(
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' ||
node.type === 'VariableDeclaration'
)
}
================================================
FILE: packages/orgx/lib/util/estree-util-specifiers-to-declarations.js
================================================
/**
* @typedef {import('estree-jsx').AssignmentProperty} AssignmentProperty
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').Identifier} Identifier
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportNamespaceSpecifier} ImportNamespaceSpecifier
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*/
import { create } from './estree-util-create.js'
/**
* @param {Array} specifiers
* @param {Expression} init
* @returns {Array}
*/
export function specifiersToDeclarations(specifiers, init) {
let index = -1
/** @type {Array} */
const declarations = []
/** @type {Array} */
const otherSpecifiers = []
// Can only be one according to JS syntax.
/** @type {ImportNamespaceSpecifier | undefined} */
let importNamespaceSpecifier
while (++index < specifiers.length) {
const specifier = specifiers[index]
if (specifier.type === 'ImportNamespaceSpecifier') {
importNamespaceSpecifier = specifier
} else {
otherSpecifiers.push(specifier)
}
}
if (importNamespaceSpecifier) {
/** @type {VariableDeclarator} */
const declarator = {
type: 'VariableDeclarator',
id: importNamespaceSpecifier.local,
init
}
create(importNamespaceSpecifier, declarator)
declarations.push(declarator)
}
declarations.push({
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: otherSpecifiers.map((specifier) => {
/** @type {Identifier} */
// @ts-expect-error: fine.
let key =
specifier.type === 'ImportSpecifier'
? specifier.imported
: specifier.type === 'ExportSpecifier'
? specifier.exported
: { type: 'Identifier', name: 'default' }
let value = specifier.local
// Switch them around if we’re exporting.
if (specifier.type === 'ExportSpecifier') {
value = key
// @ts-expect-error: fine.
key = specifier.local
}
/** @type {AssignmentProperty} */
const property = {
type: 'Property',
kind: 'init',
// @ts-expect-error: fine.
shorthand: key.name === value.name,
method: false,
computed: false,
key,
// @ts-expect-error: fine.
value
}
create(specifier, property)
return property
})
},
init: importNamespaceSpecifier
? { type: 'Identifier', name: importNamespaceSpecifier.local.name }
: init
})
return declarations
}
================================================
FILE: packages/orgx/lib/util/estree-util-to-binary-addition.js
================================================
/**
* @typedef {import('estree-jsx').Expression} Expression
*/
/**
* @param {Array} expressions
*/
export function toBinaryAddition(expressions) {
let index = -1
/** @type {Expression | undefined} */
let left
while (++index < expressions.length) {
const right = expressions[index]
left = left
? { type: 'BinaryExpression', left, operator: '+', right }
: right
}
// Just for types.
/* c8 ignore next */
if (!left) throw new Error('Expected non-empty `expressions` to be passed')
return left
}
================================================
FILE: packages/orgx/lib/util/estree-util-to-id-or-member-expression.js
================================================
/**
* @typedef {import('estree-jsx').Identifier} Identifier
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
* @typedef {import('estree-jsx').Literal} Literal
* @typedef {import('estree-jsx').MemberExpression} MemberExpression
*/
import {
cont as esCont,
start as esStart,
name as isIdentifierName
} from 'estree-util-is-identifier-name'
export const toIdOrMemberExpression = toIdOrMemberExpressionFactory(
'Identifier',
'MemberExpression',
isIdentifierName
)
export const toJsxIdOrMemberExpression =
// @ts-expect-error: fine
/** @type {(ids: Array) => JSXIdentifier | JSXMemberExpression)} */
(
toIdOrMemberExpressionFactory(
'JSXIdentifier',
'JSXMemberExpression',
isJsxIdentifierName
)
)
/**
* @param {string} idType
* @param {string} memberType
* @param {(value: string) => boolean} isIdentifier
*/
function toIdOrMemberExpressionFactory(idType, memberType, isIdentifier) {
return toIdOrMemberExpression
/**
* @param {Array} ids
* @returns {Identifier | MemberExpression}
*/
function toIdOrMemberExpression(ids) {
let index = -1
/** @type {Identifier | Literal | MemberExpression | undefined} */
let object
while (++index < ids.length) {
const name = ids[index]
const valid = typeof name === 'string' && isIdentifier(name)
// A value of `asd.123` could be turned into `asd['123']` in the JS form,
// but JSX does not have a form for it, so throw.
/* c8 ignore next 3 */
if (idType === 'JSXIdentifier' && !valid) {
throw new Error(`Cannot turn \`${name}\` into a JSX identifier`)
}
/** @type {Identifier | Literal} */
// @ts-expect-error: JSX is fine.
const id = valid
? { type: idType, name }
: { type: 'Literal', value: name }
// @ts-expect-error: JSX is fine.
object = object
? {
type: memberType,
object,
property: id,
computed: id.type === 'Literal',
optional: false
}
: id
}
// Just for types.
/* c8 ignore next 3 */
if (!object) throw new Error('Expected non-empty `ids` to be passed')
if (object.type === 'Literal')
throw new Error('Expected identifier as left-most value')
return object
}
}
/**
* Checks if the given string is a valid JSX identifier name.
* @param {string} name
*/
function isJsxIdentifierName(name) {
let index = -1
while (++index < name.length) {
// We currently receive valid input, but this catches bugs and is needed
// when externalized.
/* c8 ignore next */
if (!(index ? jsxCont : esStart)(name.charCodeAt(index))) return false
}
// `false` if `name` is empty.
return index > 0
}
/**
* Checks if the given character code can continue a JSX identifier.
* @param {number} code
*/
function jsxCont(code) {
return code === 45 /* `-` */ || esCont(code)
}
================================================
FILE: packages/orgx/lib/util/is-org-content.js
================================================
/**
* Check if a node is an org content node.
* @param {import('react').ReactNode} node
* @returns {boolean}
*/
export function isOrgContent(node) {
return (
!!node &&
typeof node === 'object' &&
'type' in node &&
typeof node.type === 'function' &&
node.type.name === 'OrgContent'
)
}
================================================
FILE: packages/orgx/lib/util/render-error.js
================================================
/**
* @param {SyntaxError} error
* @param {string} bg
* @param {string} color
* @returns {import('estree-jsx').Program}
*/
export default (error, bg = '#F44336', color = 'white') => {
return {
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
attributes: [
{
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: 'style'
},
value: {
type: 'JSXExpressionContainer',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: {
type: 'Identifier',
name: 'backgroundColor'
},
value: {
type: 'Literal',
value: bg,
raw: `'${bg}'`
},
kind: 'init'
},
{
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: {
type: 'Identifier',
name: 'color'
},
value: {
type: 'Literal',
value: color,
raw: `'${color}'`
},
kind: 'init'
}
]
}
}
}
],
name: {
type: 'JSXIdentifier',
name: 'pre'
},
selfClosing: false
},
closingElement: {
type: 'JSXClosingElement',
name: {
type: 'JSXIdentifier',
name: 'pre'
}
},
children: [
{
type: 'JSXText',
value: error.message,
raw: error.message
}
]
}
}
],
sourceType: 'module'
}
}
================================================
FILE: packages/orgx/lib/util/resolve-evaluate-options.js
================================================
/**
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
*
* @typedef RunnerOptions
* Configuration with JSX runtime.
* @property {any} Fragment
* Symbol to use for fragments.
* @property {any} [jsx]
* Function to generate an element with static children in production mode.
* @property {any} [jsxs]
* Function to generate an element with dynamic children in production mode.
* @property {any} [jsxDEV]
* Function to generate an element in development mode.
* @property {any} [useOrgComponents]
* Function to get `MDXComponents` from context.
*
* @typedef {Omit } EvaluateProcessorOptions
* Compile configuration without JSX options for evaluation.
*
* @typedef {EvaluateProcessorOptions & RunnerOptions} EvaluateOptions
* Configuration for evaluation.
*/
/**
* Split compiletime options from runtime options.
*
* @param {EvaluateOptions | null | undefined} options
* @returns {{compiletime: ProcessorOptions, runtime: RunnerOptions}}
*/
export function resolveEvaluateOptions(options) {
const {
development,
Fragment,
jsx,
jsxs,
jsxDEV,
useOrgComponents,
...rest
} = options || {}
if (!Fragment) throw new Error('Expected `Fragment` given to `evaluate`')
if (development) {
if (!jsxDEV) throw new Error('Expected `jsxDEV` given to `evaluate`')
} else {
if (!jsx) throw new Error('Expected `jsx` given to `evaluate`')
if (!jsxs) throw new Error('Expected `jsxs` given to `evaluate`')
}
return {
compiletime: {
...rest,
development,
outputFormat: 'function-body',
providerImportSource: useOrgComponents ? '#' : undefined
},
runtime: {
Fragment,
jsx,
jsxs,
jsxDEV,
useOrgComponents
}
}
}
================================================
FILE: packages/orgx/lib/util/resolve-file-and-options.js
================================================
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('../compile.js').CompileOptions} CompileOptions
*/
import { VFile } from 'vfile'
/**
* Create a file and options from a given `vfileCompatible` and options that
* might contain `format: 'detect'`.
*
* @param {VFileCompatible} vfileCompatible
* @param {CompileOptions | null | undefined} [options]
* @returns {{file: VFile, options: ProcessorOptions}}
*/
export function resolveFileAndOptions(vfileCompatible, options) {
const file = looksLikeAVFile(vfileCompatible)
? vfileCompatible
: new VFile(vfileCompatible)
return {
file,
options: {
...options
}
}
}
/**
* @param {VFileCompatible | null | undefined} [value]
* @returns {value is VFile}
*/
function looksLikeAVFile(value) {
return Boolean(
value &&
typeof value === 'object' &&
'message' in value &&
'messages' in value
)
}
================================================
FILE: packages/orgx/package.json
================================================
{
"name": "@orgajs/orgx",
"version": "2.6.1",
"description": "orga compiler with jsx support",
"author": "Xiaoxing Hu ",
"license": "MIT",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/orgx#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/orgx"
},
"type": "module",
"files": [
"lib/",
"index.js",
"index.d.ts",
"index.d.ts.map",
"types.d.ts"
],
"exports": {
".": "./index.js"
},
"scripts": {
"test": "node --test tests/*.test.js"
},
"devDependencies": {
"@types/estree": "^1.0.6",
"@types/estree-jsx": "^1.0.5",
"@types/hast": "^3.0.4",
"@types/node": "^25.3.2",
"@types/react": "^19.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.9.2"
},
"dependencies": {
"@orgajs/reorg-parse": "workspace:^",
"@orgajs/reorg-rehype": "workspace:^",
"acorn": "^8.14.0",
"acorn-jsx": "^5.3.2",
"astring": "^1.8.6",
"estree-util-build-jsx": "3.0.1",
"estree-util-is-identifier-name": "3.0.0",
"estree-util-to-js": "^2.0.0",
"estree-walker": "3.0.3",
"hast-util-to-estree": "^3.1.1",
"periscopic": "3.1.0",
"recma-build-jsx": "^1.0.0",
"recma-jsx": "^1.0.0",
"recma-stringify": "^1.0.0",
"source-map": "^0.7.4",
"unified": "^11.0.5",
"unist-util-position-from-estree": "^2.0.0",
"unist-util-stringify-position": "^4.0.0",
"vfile": "^6.0.3"
}
}
================================================
FILE: packages/orgx/tests/compile.test.js
================================================
import * as assert from 'node:assert'
import { describe, it } from 'node:test'
import { compile } from '../lib/compile.js'
const fixture = `
#+title: Hello World
* Hi
`
const code = `
/*@jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment*/
import React from "react";
export const title = 'Hello World';
function _createOrgContent(props) {
const _components = Object.assign({
div: "div",
h1: "h1"
}, props.components);
return <_components.div className="section"><_components.h1>{"Hi"};
}
function OrgContent(props = {}) {
const {wrapper: OrgLayout} = props.components || ({});
return OrgLayout ? <_createOrgContent {...props} /> : _createOrgContent(props);
}
export default OrgContent;
`
describe('compile', () => {
it('can compile org file', async () => {
const result = await compile(fixture, {
jsxRuntime: 'classic',
jsx: true,
outputFormat: 'program'
})
assert.strictEqual(`${result}`.trim(), code.trim())
})
})
================================================
FILE: packages/orgx/tests/evaluate.test.js
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import { createElement } from 'react'
import * as runtime from 'react/jsx-runtime'
import { renderToStaticMarkup } from 'react-dom/server'
import { evaluate } from '../lib/evaluate.js'
describe('evaluate', () => {
it('can evaluate org file', async () => {
const text = `
* hi
`
const Content = (await evaluate(text, runtime)).default
const rendered = renderToStaticMarkup(createElement(Content))
assert.equal(rendered, '
hi
')
})
})
================================================
FILE: packages/orgx/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/react/CHANGELOG.md
================================================
# Change Log
## 4.2.1
### Patch Changes
- bd2365a: fix types and linting
## 4.2.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
## 4.1.2
### Patch Changes
- e3ef3a5: build website with orga-build
## 4.1.1
### Patch Changes
- df80497a: Fix package
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
## 3.0.1
### Patch Changes
- 8c6f440b: - better layout support
- rename MDXxxx to Orgaxxx
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
## 2.4.9 (2021-07-13)
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
**Note:** Version bump only for package @orgajs/react
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package @orgajs/react
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package @orgajs/react
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package @orgajs/react
================================================
FILE: packages/react/LICENSE.org
================================================
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/react/index.js
================================================
/**
* @typedef {import('react').ReactNode} ReactNode
* @typedef {import('@orgajs/orgx').OrgComponents} Components
*
* @typedef Props
* Configuration.
* @property {Components | MergeComponents | null | undefined} [components]
* Mapping of names for JSX components to React components.
* @property {boolean | null | undefined} [disableParentContext=false]
* Turn off outer component context.
* @property {ReactNode | null | undefined} [children]
* Children.
*
* @callback MergeComponents
* Custom merge function.
* @param {Components} currentComponents
* Current components from the context.
* @returns {Components}
* Merged components.
*/
import React from 'react'
/** @type {import('react').Context} */
const OrgContext = React.createContext({})
/**
* Get current components from the org context.
*
* @param {Components | MergeComponents | null | undefined} [components]
* Additional components to use or a function that takes the current
* components and filters/merges/changes them.
* @returns {Components}
* Current components.
*/
export function useOrgComponents(components) {
const contextComponents = React.useContext(OrgContext)
// Memoize to avoid unnecessary top-level context changes
return React.useMemo(() => {
// Custom merge via a function prop
if (typeof components === 'function') {
return components(contextComponents)
}
return { ...contextComponents, ...components }
}, [contextComponents, components])
}
/** @type {Components} */
const emptyObject = {}
/**
* Provider for org context
*
* @param {Props} props
* @returns {React.JSX.Element}
*/
export function OrgProvider({ components, children, disableParentContext }) {
const contextComponents = React.useContext(OrgContext)
const allComponents = React.useMemo(() => {
const baseComponents = disableParentContext
? emptyObject
: contextComponents
if (typeof components === 'function') {
return components(baseComponents)
}
return { ...baseComponents, ...components }
}, [components, contextComponents, disableParentContext])
return React.createElement(
OrgContext.Provider,
{ value: allComponents },
children
)
}
================================================
FILE: packages/react/package.json
================================================
{
"name": "@orgajs/react",
"version": "4.2.1",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs",
"directory": "packages/react"
},
"files": [
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"scripts": {},
"peerDependencies": {
"react": ">=16"
},
"devDependencies": {
"@babel/plugin-transform-react-jsx": "^7.22.5",
"@orgajs/orgx": "workspace:^",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"babel-plugin-remove-export-keywords": "^1.6.22",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
================================================
FILE: packages/react/tests/remove-export-keywords.js
================================================
module.exports = () => {
return {
visitor: {
ExportNamedDeclaration(path) {
const declaration = path.node.declaration
// Ignore "export { Foo as default }" syntax
if (declaration) {
path.replaceWith(declaration)
}
}
}
}
}
================================================
FILE: packages/react/tests/test.tsx
================================================
/* @jsx React.createElement */
/* @jsxFrag React.Fragment */
import { transformAsync as babelTransform } from '@babel/core'
import toJsx from '@orgajs/estree-jsx'
import toEstree from '@orgajs/rehype-estree'
import reorg from '@orgajs/reorg'
import removeExport from 'babel-plugin-remove-export-keywords'
import { renderToString } from 'react-dom/server'
import { orga } from '../src'
const run = async (value) => {
const processor = reorg().use(toHast).use(toEstree).use(toJsx)
const doc = await processor.process(value)
console.log({ doc })
// …and that into serialized JS.
const { code } = await babelTransform(doc.toString('utf-8'), {
configFile: false,
plugins: ['@babel/plugin-transform-react-jsx', removeExport]
})
console.dir(code)
// …and finally run it, returning the component.
// eslint-disable-next-line no-new-func
return new Function('orga', `${code}; return OrgaContent`)(orga)
}
describe.skip('@orgajs/react', () => {
test('should work', async () => {
const Content = await run('* hi')
const string = renderToString( )
console.dir(string)
})
})
================================================
FILE: packages/react/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/react-cm/CHANGELOG.md
================================================
# @orgajs/react-cm
## 0.1.3
### Patch Changes
- bd2365a: fix types and linting
## 0.1.2
### Patch Changes
- 94ae77a: fix cursor jumping issue
## 0.1.1
### Patch Changes
- 60ad38f: migrate orga-build to be based on vite
================================================
FILE: packages/react-cm/index.js
================================================
import { EditorState } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { useEffect, useRef } from 'react'
import { jsx } from 'react/jsx-runtime'
/**
* A React component that renders an Orga editor.
*
* @param {Object} props - The component props
* @param {string} [props.content=''] - The initial content for the editor
* @param {string} [props.className] - CSS class name to apply to the editor container
* @param {Function} [props.onChange] - Callback function that receives the updated content when changes occur
* @param {import('@codemirror/state').Extension} [props.extensions] - Array of CodeMirror extensions to customize the editor
* @returns {import('react').ReactElement} A div element containing the editor
*/
export function ReactCodeMirror({
className = '',
content = '',
extensions = [],
onChange
}) {
/** @type {import('react').RefObject} */
const container = useRef(undefined)
/** @type {import('react').RefObject} */
const editor = useRef(undefined)
/** @type {import('react').RefObject} */
const onChangeRef = useRef(onChange)
/** @type {import('react').RefObject} */
const initialContent = useRef(content)
/** @type {import('react').RefObject} */
const initialExtensions = useRef(extensions)
useEffect(() => {
onChangeRef.current = onChange
}, [onChange])
useEffect(() => {
if (!container.current || editor.current) return
const state = EditorState.create({
doc: initialContent.current,
extensions: initialExtensions.current
})
const ed = new EditorView({
state,
parent: container.current,
dispatch(tr) {
ed.update([tr])
if (tr.docChanged) {
onChangeRef.current?.(ed.state)
}
}
})
editor.current = ed
return () => {
ed.destroy()
editor.current = undefined
}
}, [])
useEffect(() => {
if (!editor.current) return
if (editor.current.state.doc.toString() === content) return
editor.current.dispatch({
changes: {
from: 0,
to: editor.current.state.doc.length,
insert: content
}
})
}, [content])
return jsx('div', { ref: container, className })
}
================================================
FILE: packages/react-cm/package.json
================================================
{
"name": "@orgajs/react-cm",
"version": "0.1.3",
"type": "module",
"description": "",
"main": "index.js",
"scripts": {},
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs",
"directory": "packages/react-cm"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.4.1",
"devDependencies": {
"@types/react": "^19.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"dependencies": {
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.2"
}
}
================================================
FILE: packages/react-editor/CHANGELOG.md
================================================
# @orgajs/react-editor
## 0.1.3
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- @orgajs/react-cm@0.1.3
- @orgajs/editor@1.4.1
## 0.1.2
### Patch Changes
- Updated dependencies [a53cfea]
- @orgajs/editor@1.4.0
## 0.1.1
### Patch Changes
- 60ad38f: migrate orga-build to be based on vite
- Updated dependencies [60ad38f]
- @orgajs/react-cm@0.1.1
- @orgajs/editor@1.3.1
================================================
FILE: packages/react-editor/index.js
================================================
import { setup } from '@orgajs/editor'
import { ReactCodeMirror } from '@orgajs/react-cm'
import { jsx } from 'react/jsx-runtime'
/**
* A React component that renders an Orga editor.
*
* @param {Object} props - The component props
* @param {string} [props.content=''] - The initial content for the editor
* @param {string} [props.className] - CSS class name to apply to the editor container
* @param {Function} [props.onChange] - Callback function that receives the updated content when changes occur
* @param {import('@codemirror/state').Extension} [props.extensions] - Array of CodeMirror extensions to customize the editor
* @returns {import('react').ReactElement} A div element containing the editor
*/
export function Editor({ content = '', className, onChange, extensions = [] }) {
return jsx(ReactCodeMirror, {
className,
content,
onChange,
extensions: [setup, extensions]
})
}
================================================
FILE: packages/react-editor/package.json
================================================
{
"name": "@orgajs/react-editor",
"version": "0.1.3",
"type": "module",
"description": "@orgajs/editor in react",
"files": [
"index.js"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"scripts": {},
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/react-editor"
},
"keywords": [
"org-mode",
"react",
"editor"
],
"author": "Xiaoxing Hu ",
"license": "MIT",
"packageManager": "pnpm@10.4.1",
"dependencies": {
"@orgajs/editor": "workspace:^",
"@orgajs/react-cm": "workspace:^"
},
"devDependencies": {
"@codemirror/state": "^6.5.2",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"esbuild": "^0.24.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
================================================
FILE: packages/react-editor/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
================================================
FILE: packages/reorg/CHANGELOG.md
================================================
# Change Log
## 4.3.3
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- @orgajs/reorg-parse@4.4.1
- orga@4.7.1
## 4.3.2
### Patch Changes
- Updated dependencies [a53cfea]
- @orgajs/reorg-parse@4.4.0
## 4.3.1
### Patch Changes
- @orgajs/reorg-parse@4.3.1
## 4.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- @orgajs/reorg-parse@4.3.0
## 4.2.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- @orgajs/reorg-parse@4.2.0
## 4.1.2
### Patch Changes
- @orgajs/reorg-parse@4.1.2
## 4.1.1
### Patch Changes
- @orgajs/reorg-parse@4.1.1
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- @orgajs/reorg-parse@4.1.0
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- @orgajs/reorg-parse@4.0.0
## 3.1.7
### Patch Changes
- @orgajs/reorg-parse@3.1.7
## 3.1.6
### Patch Changes
- @orgajs/reorg-parse@3.1.6
## 3.1.5
### Patch Changes
- @orgajs/reorg-parse@3.1.5
## 3.1.4
### Patch Changes
- @orgajs/reorg-parse@3.1.4
## 3.1.3
### Patch Changes
- @orgajs/reorg-parse@3.1.3
## 3.1.2
### Patch Changes
- @orgajs/reorg-parse@3.1.2
## 3.1.1
### Patch Changes
- @orgajs/reorg-parse@3.1.1
## 3.1.0
### Minor Changes
- eeea0c54: introduce new token: empty line
### Patch Changes
- Updated dependencies [eeea0c54]
- @orgajs/reorg-parse@3.1.0
## 3.0.1
### Patch Changes
- 6ed76057: # rename gatsby themes
- gatsby-theme-orga -> gatsby-theme-orga-posts-core
- gatsby-theme-blorg -> gatsby-theme-orga-posts
# add example projects
- gatsby-posts
- gatsby-posts-core
- 759e6149: # Bug Fixes
- fix lexer for parsing headline with todo keyword
- fix properties drawer issue
- fix orga-theme-ui-preset package
- fix gatsby-transformer-orga & gatsby-theme-blorg
# Improved Playground
- add `tokens` view
- show node type in tree views
- Updated dependencies [6ed76057]
- Updated dependencies [759e6149]
- @orgajs/reorg-parse@3.0.1
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
### Patch Changes
- Updated dependencies [8b02d10]
- @orgajs/reorg-parse@3.0.0
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- lock unified & @types/unist version ([dc72217](https://github.com/orgapp/orgajs/commit/dc72217f0cfcd778436d704021116c8479f8ee1e))
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
## 2.4.9 (2021-07-13)
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
### Bug Fixes
- lock unified & @types/unist version ([dc72217](https://github.com/orgapp/orgajs/commit/dc72217f0cfcd778436d704021116c8479f8ee1e))
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package @orgajs/reorg
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg
================================================
FILE: packages/reorg/LICENSE.org
================================================
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/reorg/README.org
================================================
#+TITLE: @orgajs/reorg
[[https://github.com/unifiedjs/unified][unified]] processor to parse [[https://orgmode.org][org-mode]].
================================================
FILE: packages/reorg/index.js
================================================
import parse from '@orgajs/reorg-parse'
import { unified } from 'unified'
export const reorg = unified().use(parse).freeze()
================================================
FILE: packages/reorg/package.json
================================================
{
"name": "@orgajs/reorg",
"version": "4.3.3",
"description": "orga processor for unifiedjs",
"files": [
"lib",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"type": "module",
"author": "Xiaoxing Hu ",
"license": "MIT",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/reorg#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/reorg"
},
"scripts": {},
"dependencies": {
"@orgajs/reorg-parse": "workspace:^",
"orga": "workspace:^",
"unified": "^11.0.5"
},
"devDependencies": {
"@types/unist": "^3.0.3"
}
}
================================================
FILE: packages/reorg-parse/CHANGELOG.md
================================================
# Change Log
## 4.4.1
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- orga@4.7.1
## 4.4.0
### Minor Changes
- a53cfea: all about the editor
This release improves the editor with new fold/shift/todo actions and settings, while also refactoring orga tokenization/parsing and lezer conversion to improve TODO handling, context hashing, and tree generation consistency.
### Patch Changes
- Updated dependencies [a53cfea]
- orga@4.6.0
## 4.3.1
### Patch Changes
- Updated dependencies [60ad38f]
- orga@4.5.1
## 4.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- orga@4.5.0
## 4.2.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- orga@4.4.0
## 4.1.2
### Patch Changes
- Updated dependencies [7cfff79a]
- orga@4.3.0
## 4.1.1
### Patch Changes
- Updated dependencies [ac322714]
- orga@4.2.0
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- orga@4.1.0
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- orga@4.0.0
## 3.1.7
### Patch Changes
- Updated dependencies [eeccc870]
- orga@3.2.1
## 3.1.6
### Patch Changes
- Updated dependencies [6c1ddb9f]
- orga@3.2.0
## 3.1.5
### Patch Changes
- Updated dependencies [4bde5155]
- orga@3.1.5
## 3.1.4
### Patch Changes
- Updated dependencies [ae83a3b0]
- orga@3.1.4
## 3.1.3
### Patch Changes
- Updated dependencies [09a3b5c6]
- orga@3.1.3
## 3.1.2
### Patch Changes
- Updated dependencies [594bf16b]
- orga@3.1.2
## 3.1.1
### Patch Changes
- Updated dependencies [19156b8a]
- orga@3.1.1
## 3.1.0
### Minor Changes
- eeea0c54: introduce new token: empty line
### Patch Changes
- Updated dependencies [eeea0c54]
- orga@3.1.0
## 3.0.1
### Patch Changes
- 6ed76057: # rename gatsby themes
- gatsby-theme-orga -> gatsby-theme-orga-posts-core
- gatsby-theme-blorg -> gatsby-theme-orga-posts
# add example projects
- gatsby-posts
- gatsby-posts-core
- 759e6149: # Bug Fixes
- fix lexer for parsing headline with todo keyword
- fix properties drawer issue
- fix orga-theme-ui-preset package
- fix gatsby-transformer-orga & gatsby-theme-blorg
# Improved Playground
- add `tokens` view
- show node type in tree views
- Updated dependencies [6ed76057]
- Updated dependencies [759e6149]
- orga@3.0.1
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
### Patch Changes
- Updated dependencies [8b02d10]
- orga@3.0.0
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- lock unified & @types/unist version ([dc72217](https://github.com/orgapp/orgajs/commit/dc72217f0cfcd778436d704021116c8479f8ee1e))
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
## 2.4.9 (2021-07-13)
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
### Bug Fixes
- lock unified & @types/unist version ([dc72217](https://github.com/orgapp/orgajs/commit/dc72217f0cfcd778436d704021116c8479f8ee1e))
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package @orgajs/reorg-parse
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg-parse
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg-parse
================================================
FILE: packages/reorg-parse/LICENSE.org
================================================
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/reorg-parse/README.org
================================================
#+TITLE: @orgajs/reorg-parse
Parser for [[https://github.com/unifiedjs/unified][unified]]. Parses org-mode to oast syntax trees. Used in [[https://github.com/orgapp/orgajs/tree/main/packages/reorg][@orgajs/reorg]] processor.
================================================
FILE: packages/reorg-parse/index.d.ts
================================================
import type { Document, ParseOptions } from 'orga'
import type { Plugin } from 'unified'
declare const reorgParse: Plugin<
[(Readonly | null | undefined)?],
string,
Document
>
export default reorgParse
================================================
FILE: packages/reorg-parse/index.js
================================================
/**
* @import {Document, ParseOptions} from 'orga'
* @import {Processor} from 'unified'
*/
/**
* @typedef {ParseOptions} Options
*/
import { parse } from 'orga'
/**
* Aadd support for parsing from org-mode.
*
* @this {Processor}
* Processor instance.
* @param {Partial | undefined} [options]
* Configuration (optional).
* @returns {undefined}
* Nothing.
*/
export default function reorgParse(options) {
this.parser = function (document) {
return parse(document, options)
}
}
================================================
FILE: packages/reorg-parse/package.json
================================================
{
"name": "@orgajs/reorg-parse",
"version": "4.4.1",
"description": "orga parser for unifiedjs",
"type": "module",
"files": [
"lib/",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"exports": "./index.js",
"author": "Xiaoxing Hu ",
"license": "MIT",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/reorg-parse#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/reorg-parse"
},
"scripts": {},
"dependencies": {
"orga": "workspace:^"
},
"devDependencies": {
"typescript": "^5.9.3",
"unified": "^11.0.5"
}
}
================================================
FILE: packages/reorg-parse/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/reorg-prose/CHANGELOG.md
================================================
# @orgajs/reorg-prose
## 1.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- oast-to-prose@1.3.0
## 1.2.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- oast-to-prose@1.2.0
## 1.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- oast-to-prose@1.1.0
================================================
FILE: packages/reorg-prose/index.js
================================================
export { schema } from 'oast-to-prose'
export { default } from './lib/index.js'
================================================
FILE: packages/reorg-prose/lib/index.js
================================================
/**
* @import {Document as OastRoot} from 'orga'
* @import {Node as ProseNode} from 'prosemirror-model'
* @import {Options} from 'oast-to-prose'
* @import {VFile} from 'vfile'
*/
import { toProse } from 'oast-to-prose'
/**
*
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export default function reorgProse(options) {
/**
* @param {OastRoot} tree
* Tree (hast).
* @param {VFile} file
* File.
* @returns {ProseNode}
* Prose Node.
*/
return function (tree, file) {
return /** @type {ProseNode} */ toProse(tree, file, options)
}
}
================================================
FILE: packages/reorg-prose/package.json
================================================
{
"name": "@orgajs/reorg-prose",
"version": "1.3.0",
"description": "",
"author": "Xiaoxing Hu ",
"license": "MIT",
"type": "module",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/reorg-prose#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/reorg-prose"
},
"files": [
"lib",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"scripts": {},
"keywords": [],
"dependencies": {
"oast-to-prose": "workspace:^"
},
"devDependencies": {
"@types/unist": "^3.0.3",
"orga": "workspace:^",
"prosemirror-model": "^1.19.3",
"unified": "^11.0.5",
"vfile": "^6.0.3"
}
}
================================================
FILE: packages/reorg-prose/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/reorg-rehype/.projectile
================================================
================================================
FILE: packages/reorg-rehype/CHANGELOG.md
================================================
# Change Log
## 4.3.11
### Patch Changes
- Updated dependencies [850bcf9]
- oast-to-hast@4.5.3
## 4.3.10
### Patch Changes
- Updated dependencies [be20652]
- oast-to-hast@4.5.2
## 4.3.9
### Patch Changes
- Updated dependencies [bd2365a]
- oast-to-hast@4.5.1
## 4.3.8
### Patch Changes
- Updated dependencies [761c484]
- oast-to-hast@4.5.0
## 4.3.7
### Patch Changes
- Updated dependencies [68430c7]
- oast-to-hast@4.4.3
## 4.3.6
### Patch Changes
- Updated dependencies [d8da621]
- oast-to-hast@4.4.2
## 4.3.5
### Patch Changes
- Updated dependencies [20f5a03]
- oast-to-hast@4.4.1
## 4.3.4
### Patch Changes
- Updated dependencies [da20dcc]
- oast-to-hast@4.4.0
## 4.3.3
### Patch Changes
- oast-to-hast@4.3.3
## 4.3.2
### Patch Changes
- oast-to-hast@4.3.2
## 4.3.1
### Patch Changes
- Updated dependencies [7c3c600]
- oast-to-hast@4.3.1
## 4.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- oast-to-hast@4.3.0
## 4.2.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- oast-to-hast@4.2.0
## 4.1.3
### Patch Changes
- Updated dependencies [ab38e4b0]
- oast-to-hast@4.1.3
## 4.1.2
### Patch Changes
- oast-to-hast@4.1.2
## 4.1.1
### Patch Changes
- oast-to-hast@4.1.1
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- oast-to-hast@4.1.0
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- oast-to-hast@4.0.0
## 3.0.10
### Patch Changes
- eeccc870: - get image links out of paragraph
- some other minor fixes
- Updated dependencies [eeccc870]
- oast-to-hast@3.2.1
## 3.0.9
### Patch Changes
- Updated dependencies [6c1ddb9f]
- oast-to-hast@3.2.0
## 3.0.8
### Patch Changes
- oast-to-hast@3.1.6
## 3.0.7
### Patch Changes
- Updated dependencies [ae83a3b0]
- oast-to-hast@3.1.5
## 3.0.6
### Patch Changes
- oast-to-hast@3.1.4
## 3.0.5
### Patch Changes
- oast-to-hast@3.1.3
## 3.0.4
### Patch Changes
- Updated dependencies [19156b8a]
- oast-to-hast@3.1.2
## 3.0.3
### Patch Changes
- 7f209ff5: export Options type
- Updated dependencies [7f209ff5]
- oast-to-hast@3.1.1
## 3.0.2
### Patch Changes
- Updated dependencies [eeea0c54]
- oast-to-hast@3.1.0
## 3.0.1
### Patch Changes
- 6ed76057: # rename gatsby themes
- gatsby-theme-orga -> gatsby-theme-orga-posts-core
- gatsby-theme-blorg -> gatsby-theme-orga-posts
# add example projects
- gatsby-posts
- gatsby-posts-core
- 759e6149: # Bug Fixes
- fix lexer for parsing headline with todo keyword
- fix properties drawer issue
- fix orga-theme-ui-preset package
- fix gatsby-transformer-orga & gatsby-theme-blorg
# Improved Playground
- add `tokens` view
- show node type in tree views
- Updated dependencies [6ed76057]
- Updated dependencies [759e6149]
- oast-to-hast@3.0.1
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
### Patch Changes
- Updated dependencies [8b02d10]
- oast-to-hast@3.0.0
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
## 2.4.9 (2021-07-13)
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
**Note:** Version bump only for package @orgajs/reorg-rehype
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package @orgajs/reorg-rehype
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg-rehype
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package @orgajs/reorg-rehype
================================================
FILE: packages/reorg-rehype/LICENSE.org
================================================
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/reorg-rehype/index.js
================================================
/**
* @import {Document} from 'orga'
* @import {Root as HastRoot} from 'hast'
* @import {VFile} from 'vfile'
*
* @typedef {import('oast-to-hast').Options} Options
*
* @callback TransformMutate
* Mutate-mode.
*
* Further transformers run on the hast tree.
* @param {Document} tree
* Tree.
* @param {VFile} file
* File.
* @returns {HastRoot}
* Tree (hast).
*/
import toHAST from 'oast-to-hast'
/**
* @param {Partial} [options]
* @returns {TransformMutate}
*/
function reorg2rehype(options = {}) {
return transformer
/**
* @type {TransformMutate}
*/
function transformer(tree) {
return /** @type {HastRoot} */ (toHAST(tree, options))
}
}
export default reorg2rehype
================================================
FILE: packages/reorg-rehype/package.json
================================================
{
"name": "@orgajs/reorg-rehype",
"version": "4.3.11",
"description": "rehype support for orga",
"files": [
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"exports": "./index.js",
"type": "module",
"author": "Xiaoxing Hu ",
"license": "MIT",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/reorg-rehype#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/reorg-rehype"
},
"scripts": {},
"dependencies": {
"oast-to-hast": "workspace:*"
},
"devDependencies": {
"@types/hast": "^3.0.4",
"@types/unist": "^3.0.3",
"orga": "workspace:*",
"unified": "^11.0.5",
"vfile": "^6.0.3"
}
}
================================================
FILE: packages/reorg-rehype/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/rollup/CHANGELOG.md
================================================
# @orgajs/rollup
## 1.3.4
### Patch Changes
- bd2365a: fix types and linting
- Updated dependencies [bd2365a]
- @orgajs/orgx@2.6.1
## 1.3.3
### Patch Changes
- Updated dependencies [a53cfea]
- @orgajs/orgx@2.6.0
## 1.3.2
### Patch Changes
- 60ad38f: migrate orga-build to be based on vite
- @orgajs/orgx@2.5.2
## 1.3.1
### Patch Changes
- @orgajs/orgx@2.5.1
## 1.3.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
### Patch Changes
- Updated dependencies [188d30f]
- @orgajs/orgx@2.5.0
## 1.2.2
### Patch Changes
- e3ef3a5: build website with orga-build
- Updated dependencies [e3ef3a5]
- @orgajs/orgx@2.4.1
## 1.2.1
### Patch Changes
- Updated dependencies [351f690]
- @orgajs/orgx@2.4.0
## 1.2.0
### Minor Changes
- d8861c2: update unified ecosystem
### Patch Changes
- Updated dependencies [d8861c2]
- @orgajs/orgx@2.3.0
## 1.1.3
### Patch Changes
- @orgajs/orgx@2.2.2
## 1.1.2
### Patch Changes
- @orgajs/orgx@2.2.1
## 1.1.1
### Patch Changes
- Updated dependencies [ac322714]
- @orgajs/orgx@2.2.0
## 1.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
### Patch Changes
- Updated dependencies [4d8efbb7]
- @orgajs/orgx@2.1.0
## 1.0.1
### Patch Changes
- Updated dependencies [1dbf674d]
- @orgajs/orgx@2.0.1
## 1.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
### Patch Changes
- Updated dependencies [176a3b5d]
- @orgajs/orgx@2.0.0
================================================
FILE: packages/rollup/index.js
================================================
/**
* @typedef {import('@rollup/pluginutils').FilterPattern} FilterPattern
* @typedef {import('rollup').SourceDescription} SourceDescription
* @typedef Plugin
* Plugin that is compatible with both Rollup and Vite.
* @property {string} name
* The name of the plugin
* @property {ViteConfig} config
* Function used by Vite to set additional configuration options.
* @property {Transform} transform
* Function to transform the source content.
*
* @callback Transform
* Callback called by Rollup and Vite to transform.
* @param {string} value
* File contents.
* @param {string} path
* File path.
* @returns {Promise}
* Result.
*
* @callback ViteConfig
* Callback called by Vite to set additional configuration options.
* @param {unknown} config
* Configuration object (unused).
* @param {ViteEnv} env
* Environment variables.
* @returns {undefined}
* Nothing.
*
* @typedef ViteEnv
* Environment variables used by Vite.
* @property {string} mode
* Mode.
*/
/**
* @typedef {Omit} CompileOptions
* Default configuration.
*
* @typedef RollupPluginOptions
* Extra configuration.
* @property {FilterPattern} [include]
* List of picomatch patterns to include
* @property {FilterPattern} [exclude]
* List of picomatch patterns to exclude
*
* @typedef {CompileOptions & RollupPluginOptions} Options
* Configuration.
*/
import { createProcessor } from '@orgajs/orgx'
import { createFilter } from '@rollup/pluginutils'
import { SourceMapGenerator } from 'source-map'
import { VFile } from 'vfile'
/**
* Compile org-mode w/ rollup.
*
* @param {Options | null | undefined} [options]
* Configuration.
* @return {Plugin}
* Rollup plugin.
*/
export default function rollup(options) {
const { include, exclude, ...rest } = options || {}
/** @type {ReturnType} */
let processor
const filter = createFilter(include, exclude)
return {
name: '@orgajs/rollup',
config(_config, env) {
processor = createProcessor({
SourceMapGenerator,
development: env.mode === 'development',
...rest
})
},
async transform(value, path) {
processor ||= createProcessor({
SourceMapGenerator,
...rest
})
const file = new VFile({ value, path })
if (file.extname === '.org' && filter(file.path)) {
const compiled = await processor.process(file)
const code = String(compiled.value)
/** @type {SourceDescription} */
const result = {
code,
map: compiled.map
}
return result
}
}
}
}
================================================
FILE: packages/rollup/package.json
================================================
{
"name": "@orgajs/rollup",
"version": "1.3.4",
"description": "Rollup plugin for Orga",
"license": "MIT",
"files": [
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"type": "module",
"author": "Xiaoxing Hu ",
"homepage": "https://orga.js.org",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/rollup"
},
"scripts": {
"test": "node --test test.js"
},
"devDependencies": {
"@types/node": "^25.3.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rollup": "^4.59.0"
},
"peerDependencies": {
"rollup": ">=2"
},
"dependencies": {
"@orgajs/orgx": "workspace:^",
"@rollup/pluginutils": "^5.1.4",
"source-map": "^0.7.4",
"vfile": "^6.0.3"
}
}
================================================
FILE: packages/rollup/test.js
================================================
import assert from 'node:assert'
import { promises as fs } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { rollup } from 'rollup'
import rollupOrg from './index.js'
test('@orgajs/rollup', async () => {
await fs.writeFile(new URL('rollup.org', import.meta.url), '* Hi')
const bundle = await rollup({
input: fileURLToPath(new URL('rollup.org', import.meta.url)),
external: ['react/jsx-runtime'],
plugins: [rollupOrg()]
})
const { output } = await bundle.generate({ format: 'es', sourcemap: true })
await fs.writeFile(new URL('rollup.js', import.meta.url), output[0].code)
/* @ts-expect-error file is dynamically generated */
const { default: Content } = await import('./rollup.js')
// T.is(output[0].map ? output[0].map.mappings : undefined, undefined)
assert.equal(
renderToStaticMarkup(createElement(Content)),
'
Hi '
)
await fs.unlink(new URL('rollup.org', import.meta.url))
await fs.unlink(new URL('rollup.js', import.meta.url))
})
================================================
FILE: packages/rollup/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: packages/text-kit/CHANGELOG.md
================================================
# Change Log
## 4.5.1
### Patch Changes
- bd2365a: fix types and linting
## 4.5.0
### Minor Changes
- a53cfea: all about the editor
This release improves the editor with new fold/shift/todo actions and settings, while also refactoring orga tokenization/parsing and lezer conversion to improve TODO handling, context hashing, and tree generation consistency.
## 4.4.0
### Minor Changes
- 188d30f: - migrate most of modules to js
- fix types during the process
- remove unmaintained modules
## 4.3.0
### Minor Changes
- d8861c2: update unified ecosystem
## 4.2.0
### Minor Changes
- ac322714: implement editor
## 4.1.0
### Minor Changes
- 4d8efbb7: Add increamental parsing ability for the editor.
## 4.0.0
### Major Changes
- 176a3b5d: # Migrate most of the ecosystem to ESM
We are excited to announce that we have migrated most of our ecosystem to ESM! This move was necessary as the unified ecosystem had already transitioned to ESM, leaving our orgajs system stuck on an older version if we wanted to stay on commonjs. We understand that this transition may come with some inevitable breaking changes, but we have done our best to make it as gentle as possible.
In the past, ESM support in popular frameworks like webpack, gatsby, and nextjs was problematic, but the JS world has steadily moved forward, and we are now in a much better state. We have put in a lot of effort to bring this project up to speed, and we are happy to say that it's in a pretty good state now.
We acknowledge that there are still some missing features that we will gradually add back over time. However, we feel that the changes are now in a great state to be released to the world. If you want to use the new versions, we recommend checking out the `examples` folder to get started.
We understand that this upgrade path may not be compatible with older versions, and we apologize for any inconvenience this may cause. However, we encourage you to consider starting fresh, as the most important part of your site should always be your content (org-mode files). Thank you for your understanding, and we hope you enjoy the new and improved ecosystem!
## 3.0.2
### Patch Changes
- eeccc870: - get image links out of paragraph
- some other minor fixes
## 3.0.1
### Patch Changes
- 6ed76057: # rename gatsby themes
- gatsby-theme-orga -> gatsby-theme-orga-posts-core
- gatsby-theme-blorg -> gatsby-theme-orga-posts
# add example projects
- gatsby-posts
- gatsby-posts-core
- 759e6149: # Bug Fixes
- fix lexer for parsing headline with todo keyword
- fix properties drawer issue
- fix orga-theme-ui-preset package
- fix gatsby-transformer-orga & gatsby-theme-blorg
# Improved Playground
- add `tokens` view
- show node type in tree views
## 3.0.0
### Major Changes
- 8b02d10: # Features
- more powerful and flexible lexer and parser
- webpack support
- `jsx` support
- better code block rendering
- better image processing in gatsby
- updated examples
- tons of bug fixes
- brand new `gatsby-plugin-orga`
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.6.0](https://github.com/orgapp/orgajs/compare/v2.5.0...v2.6.0) (2021-08-28)
### Bug Fixes
- remove prepublish step individually ([a75a6a9](https://github.com/orgapp/orgajs/commit/a75a6a9606421b66b6ef69b28e3fcb03a5ee282a))
- **website:** better code block ([9d5b3a2](https://github.com/orgapp/orgajs/commit/9d5b3a2d554672d22523727e89b2b5c60dc6233d))
# [2.5.0](https://github.com/orgapp/orgajs/compare/v2.4.9...v2.5.0) (2021-08-27)
**Note:** Version bump only for package text-kit
## [2.4.9](https://github.com/orgapp/orgajs/compare/v2.4.8...v2.4.9) (2021-07-13)
**Note:** Version bump only for package text-kit
## [2.4.8](https://github.com/orgapp/orgajs/compare/v2.4.7...v2.4.8) (2021-04-26)
**Note:** Version bump only for package text-kit
## [2.4.7](https://github.com/orgapp/orgajs/compare/v2.4.6...v2.4.7) (2021-04-26)
**Note:** Version bump only for package text-kit
================================================
FILE: packages/text-kit/README.md
================================================
# `text-kit`
> TODO: description
## Usage
```
const textKit = require('text-kit');
// TODO: DEMONSTRATE API
```
================================================
FILE: packages/text-kit/index.js
================================================
/**
* @import {Point} from 'unist'
* @typedef {ReturnType} Reader
* @typedef {object} Range
* @property {Point | number} start
* @property {Point | number} end
*/
import core from './lib/core.js'
import reader from './lib/reader.js'
/**
* @param {string} text
* @param {Partial} [range={}]
* @returns {Reader}
*/
export function read(text, range = {}) {
return reader(core(text), range)
}
================================================
FILE: packages/text-kit/lib/core.js
================================================
/**
* @import {Point,Position} from 'unist'
*/
/**
* @typedef {object} CoreAPI
* @property {string} text
* @property {number} numberOfLines
* @property {(index: number) => Point} toPoint
* @property {(point: Point | number) => number} toIndex
* @property {(ln: number) => Point | null} bol
* @property {(ln: number) => Point | null} eol
* @property {(point: Point, offset: number) => Point} shift
*/
/**
* @param {number} num
* @param {number} min
* @param {number} max
*/
function clamp(num, min, max) {
return num > max ? max : num < min ? min : num
}
/**
* @param {string} text
* @returns {CoreAPI}
*/
function core(text) {
const strLines = text.split(/^/gm)
const lines = strLines.length > 0 ? [0] : [] // index of line starts
strLines.slice(0, strLines.length - 1).forEach((l, i) => {
lines.push(lines[i] + l.length)
})
function eof() {
if (lines.length === 0) {
return { line: 1, column: 1, offset: 0 }
}
return {
line: lines.length,
column: text.length - lines[lines.length - 1] + 1,
offset: text.length
}
}
/**
* @param {number} ln
* @returns {Point | null}
*/
function bol(ln) {
const lineIndex = ln - 1
if (lineIndex >= lines.length || lineIndex < 0) return null
return { line: ln, column: 1, offset: lines[ln - 1] }
}
/**
* @param {number} ln
* @returns {Point | null}
*/
function eol(ln) {
if (ln > lines.length || ln < 1) return null
const lastLine = ln === lines.length
return lastLine ? eof() : toPoint(lines[ln] - 1)
}
/**
* @param {Point} point
* @param {number} offset
*/
function shift(point, offset) {
return toPoint(toIndex(point) + offset)
}
/**
* @param {number} index
* @returns {Point}
*/
function toPoint(index) {
if (index <= 0) return { line: 1, column: 1, offset: 0 }
if (index >= text.length) return eof()
let lineIndex = lines.findIndex((l) => l > index)
if (lineIndex < 0) {
lineIndex = lines.length
}
return {
line: lineIndex,
column: index - lines[lineIndex - 1] + 1,
offset: index
}
}
/**
* @param {Point|number} point
* @returns {number}
*/
function toIndex(point) {
if (typeof point === 'number') return clamp(point, 0, text.length)
if (point.offset) return clamp(point.offset, 0, text.length)
const lineIndex = point.line - 1
if (lineIndex < 0 || lines.length === 0) return 0
if (lineIndex >= lines.length) return text.length
const i = lines[lineIndex] + point.column - 1
return Math.min(i, text.length)
}
return {
get text() {
return text
},
get numberOfLines() {
return lines.length
},
shift,
toPoint,
toIndex,
bol,
eol
}
}
export default core
================================================
FILE: packages/text-kit/lib/reader.js
================================================
/**
* @import {Point,Position} from 'unist'
* @import {CoreAPI} from './core.js'
* @import {Range} from '../index.js'
*/
/**
* @typedef {'char'|'line'|'whitespaces'|'newline'|RegExp|number} eatable
*/
import enhance from './utils/index.js'
/**
* @type {Record}
*/
const PAIRS = [
['{', '}'],
['[', ']'],
['(', ')'],
['<', '>']
].reduce((all, [l, r]) => {
return {
...all,
[l]: r,
[r]: l
}
}, {})
/**
* @param {CoreAPI} _core
* @param {Partial} [range={}]
*/
function reader(_core, range = {}) {
const core = enhance(_core)
let cursor = core.toIndex(range.start || 0)
const end = core.toIndex(range.end || Infinity)
const getChar = (offset = 0) => {
const index = cursor + offset
if (index >= end || index < 0) return undefined
return core.text.charAt(index)
}
/**
* Get the current line
* returns null if cursor is at the end of the document
*/
function getLine() {
const pos = core.linePosition(cursor)
if (!pos) return null
return core.substring(cursor, pos.end)
}
/**
* @param {Point|number} point
*/
function jump(point) {
cursor = Math.min(end, core.toIndex(point))
}
const now = () => core.toPoint(cursor)
/**
* @param {eatable} [param='char']
* @returns {{position:Position,value:string}|undefined}
*/
function eat(param = 'char') {
let value
const position = {
start: core.toPoint(cursor),
end: core.toPoint(cursor)
}
if (cursor >= end) return undefined
if (param === 'char') {
value = getChar()
cursor += 1
} else if (param === 'line') {
const lineEnd = core.linePosition(cursor)
const e =
lineEnd === null ? end : Math.min(end, lineEnd.end.offset ?? end)
value = core.substring(cursor, e)
cursor = e
} else if (param === 'whitespaces') {
return eat(/^[ \t]+/)
} else if (param === 'newline') {
return eat(/^[\n\r]/)
} else if (typeof param === 'number') {
const adv = Math.min(param, end - cursor)
value = core.text.substring(cursor, cursor + adv)
cursor += adv
} else {
const m = match(param)
if (!m) return
if (m.position.end.offset !== undefined) cursor = m.position.end.offset
value = m.result[0]
}
position.end = core.toPoint(cursor)
return {
position,
value: value || ''
}
}
/**
* @param {RegExp} pattern
* @param {Partial} [range={}]
*/
function match(pattern, range = {}) {
const s = range.start?.offset || cursor
const e = range.end?.offset || end
const str = core.text.substring(s, e)
const m = pattern.exec(str)
if (!m) return
return {
result: m,
position: {
start: core.toPoint(cursor + m.index),
end: core.toPoint(cursor + m.index + m[0].length)
}
}
}
/**
* Find the closing pair of a character
* @param {Point|number} [index=cursor]
*/
function findClosing(index = cursor) {
let cursor = core.toIndex(index)
const opening = core.text.charAt(cursor)
if (!opening) return
const closing = PAIRS[opening] || opening
let balance = 1
cursor += 1
while (cursor < end) {
const char = core.text.charAt(cursor)
if (char === opening) {
balance += 1
}
if (char === closing) {
if (opening !== closing) {
balance -= 1
} else {
balance = 0
}
}
if (balance === 0) {
return core.toPoint(cursor)
}
cursor += 1
}
}
const isStartOfLine = () => {
return cursor === 0 || getChar(-1) === '\n'
}
/**
* Find the first index of a string
* @param {string }str
* @param {Partial} [range={}]
*/
function indexOf(str, range = {}) {
const s = range.start?.offset || cursor
const e = range.end?.offset || core.endOfLine(cursor)?.offset || end
const substr = core.text.substring(s, e)
const i = substr.indexOf(str)
if (i === -1) return null
return core.toPoint(cursor + i)
}
return {
...core,
getChar,
getLine,
eat,
jump,
match,
indexOf,
findClosing,
isStartOfLine,
now: () => {
return now()
},
beginOfLine: () => core.beginOfLine(cursor),
endOfLine: () => core.endOfLine(cursor),
/**
* Read a range of text
* @param {Partial} [range={}]
*/
read: (range = {}) => {
return reader(_core, {
start: range.start || cursor,
end: range.end || end
})
}
}
}
export default reader
================================================
FILE: packages/text-kit/lib/utils/index.js
================================================
/**
* @import {CoreAPI} from '../core.js'
*/
import lines from './lines.js'
import substring from './substring.js'
/**
* @param {CoreAPI} core
*/
export default function enhance(core) {
return substring(lines(core))
}
================================================
FILE: packages/text-kit/lib/utils/lines.js
================================================
/**
* @import {Point,Position} from 'unist'
* @import {CoreAPI} from '../core.js'
*/
/**
* @callback linePosition
* @param {Point|number} start
* @param {Point|number} [end]
* @returns {Position|null}
*
* @callback endOfLine
* @param {Point|number} ln
* @returns {Point|undefined}
*
* @callback beginOfLine
* @param {Point|number} ln
* @returns {Point|undefined}
*
*/
/**
* @template {CoreAPI} T
* @param {T} core
* @returns {T & {linePosition: linePosition, endOfLine: endOfLine, beginOfLine: beginOfLine}}
*/
export default function (core) {
/**
* @type {linePosition}
*/
function linePosition(start, end) {
let result = null
if (typeof start === 'number') {
result = linePosition(core.toPoint(start))
} else {
const s = core.bol(start.line)
const e = core.eol(start.line)
if (!s || !e) return null
result = {
start: s,
end: e
}
}
if (end && result) {
const endLR = linePosition(end)
if (!endLR) return null
result.end = endLR.end
}
return result
}
/**
* @type {endOfLine}
*/
function endOfLine(ln) {
const pos = linePosition(ln)
if (!pos) return undefined
return pos.end
}
/**
* @type {beginOfLine}
*/
const beginOfLine = (ln) => {
const pos = linePosition(ln)
if (!pos) return undefined
return pos.start
}
return {
...core,
linePosition,
endOfLine,
beginOfLine
}
}
================================================
FILE: packages/text-kit/lib/utils/substring.js
================================================
/**
* @import {Point} from 'unist'
* @import {CoreAPI} from '../core.js'
*/
/**
* @callback substring
* @param {Point|number} start
* @param {Point|number} [end]
* @returns {string}
*/
/**
* @template {CoreAPI} T
* @param {T} core
* @returns {T & {substring: substring}}
*/
export default function addSubstring(core) {
/**
* @type {substring}
*/
function substring(start, end) {
const { toIndex } = core
return core.text.substring(toIndex(start), end && toIndex(end))
}
return {
...core,
substring
}
}
================================================
FILE: packages/text-kit/package.json
================================================
{
"name": "text-kit",
"version": "4.5.1",
"description": "bundle of useful tools for manipulating text",
"author": "Xiaoxing Hu ",
"homepage": "https://github.com/orgapp/orgajs/tree/main/packages/text-kit#readme",
"repository": {
"type": "git",
"url": "https://github.com/orgapp/orgajs.git",
"directory": "packages/text-kit"
},
"license": "MIT",
"type": "module",
"files": [
"lib",
"index.js",
"index.d.ts",
"index.d.ts.map"
],
"exports": "./index.js",
"scripts": {
"test": "node --test --no-warnings tests/*.js"
},
"bugs": {
"url": "https://github.com/orgapp/orgajs/issues"
},
"devDependencies": {
"@types/unist": "3.0.3",
"tsx": "^4.19.2",
"typescript": "^5.9.2"
}
}
================================================
FILE: packages/text-kit/tests/core.js
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import core from '../lib/core.js'
describe('numberOfLines', () => {
const testCases = [
{ desc: 'empty string', input: '', expected: 0 },
{ desc: 'just a newline', input: '\n', expected: 1 },
{ desc: 'with one newline', input: 'test\n', expected: 1 },
{ desc: 'with some newlines', input: 'test1\ntest2\n', expected: 2 },
{ desc: 'starts with newline', input: '\ntest', expected: 2 },
{ desc: 'ends with newline', input: 'test\n', expected: 1 },
{ desc: 'ends with carriage return', input: 'test\r', expected: 1 },
{ desc: 'split with carriage return', input: 'test\rtest', expected: 2 }
]
testCases.forEach(({ desc, input, expected }) => {
it(desc, () => {
const c = core(input)
assert.deepStrictEqual(c.numberOfLines, expected)
})
})
})
const multilineText = ['abcd', '1234', 'xy'].join('\n')
const shared = [
{
desc: 'begining of file',
text: multilineText,
index: 0,
point: { line: 1, column: 1, offset: 0 }
},
{
desc: 'end of file',
text: multilineText,
index: 11,
point: { line: 3, column: 2, offset: 11 }
},
{
desc: 'on newline',
text: multilineText,
index: 4,
point: { line: 1, column: 5, offset: 4 }
},
{
desc: 'on last newline',
text: 'abcd\n',
index: 4,
point: { line: 1, column: 5, offset: 4 }
},
{
desc: 'no newline',
text: 'abcd',
index: 1,
point: { line: 1, column: 2, offset: 1 }
}
]
describe('toPoint', () => {
const testCases = [
...shared,
{
desc: 'before begining',
text: multilineText,
index: -1,
point: { line: 1, column: 1, offset: 0 }
},
{
desc: 'after end',
text: multilineText,
index: 20,
point: { line: 3, column: 3, offset: 12 }
},
{
desc: 'after last newline',
text: 'abcd\n',
index: 10,
point: { line: 1, column: 6, offset: 5 }
}
]
testCases.forEach(({ desc, index, point, text }) => {
it(desc, () => {
const c = core(text)
assert.deepStrictEqual(c.toPoint(index), point)
})
})
})
describe('toIndex', () => {
const testCases = [
...shared,
{
desc: 'before begining with negative offset',
text: multilineText,
index: 0,
point: { line: -1, column: 1, offset: -1 }
},
{
desc: 'before begining with negative line',
text: multilineText,
index: 0,
point: { line: -1, column: 1 }
},
{
desc: 'wrong line and column',
text: multilineText,
index: 2,
point: { line: 10, column: 99, offset: 2 }
},
{
desc: 'after end with offset',
text: multilineText,
index: 12,
point: { line: -1, column: 1, offset: 20 }
},
{
desc: 'after end with line 1',
text: multilineText,
index: 12,
point: { line: 4, column: 1 }
},
{
desc: 'after end with line 2',
text: multilineText,
index: 12,
point: { line: 10, column: 1 }
},
{
desc: 'after end with column',
text: multilineText,
index: 12,
point: { line: 3, column: 5 }
}
]
testCases.forEach(({ desc, point, index, text }) => {
it(desc, () => {
const c = core(text)
assert.deepStrictEqual(c.toIndex(point), index)
})
})
})
describe('shift', () => {
const testCases = [
{
desc: 'move forward',
point: { line: 1, column: 1 },
offset: 2,
text: multilineText,
expected: { line: 1, column: 3, offset: 2 }
},
{
desc: 'move backward',
point: { line: 2, column: 1 },
offset: -2,
text: multilineText,
expected: { line: 1, column: 4, offset: 3 }
},
{
desc: 'move beyond end',
point: { line: 2, column: 1 },
offset: 99,
text: multilineText,
expected: { line: 3, column: 3, offset: 12 }
},
{
desc: 'move beyond begining',
point: { line: 2, column: 1 },
offset: -99,
text: multilineText,
expected: { line: 1, column: 1, offset: 0 }
}
]
testCases.forEach(({ desc, point, offset, expected, text }) => {
it(desc, () => {
const c = core(text)
assert.deepStrictEqual(c.shift(point, offset), expected)
})
})
})
describe('bol', () => {
const testCases = [
{ desc: 'empty document', text: '', ln: 1, expected: null },
{
desc: 'line before start of document',
text: 'text',
ln: -1,
expected: null
},
{
desc: 'line after end of document',
text: 'text',
ln: 2,
expected: null
},
{
desc: 'some text, no newline',
text: 'text',
ln: 1,
expected: { line: 1, column: 1, offset: 0 }
},
{
desc: 'some text, newline',
text: 'text\n',
ln: 1,
expected: { line: 1, column: 1, offset: 0 }
},
{
desc: 'just a newline',
text: '\n',
ln: 1,
expected: { line: 1, column: 1, offset: 0 }
},
{
desc: 'multiline',
text: 'test1\ntest2',
ln: 2,
expected: { line: 2, column: 1, offset: 6 }
},
{
desc: 'multiline, empty line',
text: 'test1\n\n',
ln: 2,
expected: { line: 2, column: 1, offset: 6 }
}
]
testCases.forEach(({ desc, text, ln, expected }) => {
it(desc, () => {
const c = core(text)
assert.deepStrictEqual(c.bol(ln), expected)
})
})
})
describe('eol', () => {
const testCases = [
{ desc: 'empty document', text: '', ln: 1, expected: null },
{
desc: 'line before start of document',
text: 'text',
ln: -1,
expected: null
},
{
desc: 'line after end of document',
text: 'text',
ln: 2,
expected: null
},
{
desc: 'some text, no newline',
text: 'text',
ln: 1,
expected: { line: 1, column: 5, offset: 4 }
},
{
desc: 'some text, newline',
text: 'text\n',
ln: 1,
expected: { line: 1, column: 6, offset: 5 }
},
{
desc: 'just a newline',
text: '\n',
ln: 1,
expected: { line: 1, column: 2, offset: 1 }
},
{
desc: 'multiline',
text: 'text1\ntext2',
ln: 2,
expected: { line: 2, column: 6, offset: 11 }
},
{
desc: 'multiline, empty line',
text: 'text1\n\n',
ln: 2,
expected: { line: 2, column: 2, offset: 7 }
},
{
desc: 'newline (carriage return)',
text: 'text\r',
ln: 1,
expected: { line: 1, column: 6, offset: 5 }
}
]
testCases.forEach(({ desc, text, ln, expected }) => {
it(desc, () => {
const c = core(text)
assert.deepStrictEqual(c.eol(ln), expected)
})
})
})
// describe('linePosition', () => {
// const c = core('hello\nworld\n')
// it('could get by middle of line', () => {
// expect(c.linePosition(7)).toEqual({
// start: { line: 2, column: 1, offset: 6 },
// end: { line: 2, column: 7, offset: 12 },
// })
// })
// it('could get by begin of line', () => {
// expect(c.linePosition(6)).toEqual({
// start: { line: 2, column: 1, offset: 6 },
// end: { line: 2, column: 7, offset: 12 },
// })
// })
// it('could get by end of line', () => {
// expect(c.linePosition(5)).toEqual({
// start: { line: 1, column: 1, offset: 0 },
// end: { line: 2, column: 1, offset: 6 },
// })
// })
// it('could handle index out of bound', () => {
// expect(c.linePosition(12)).toEqual({
// start: { line: 2, column: 7, offset: 12 },
// end: { line: 2, column: 7, offset: 12 },
// })
// })
// it('could handle negative index', () => {
// expect(c.linePosition(-1)).toEqual({
// start: { line: 1, column: 1, offset: 0 },
// end: { line: 1, column: 1, offset: 0 },
// })
// })
// it('could handle begin of file', () => {
// expect(c.linePosition(0)).toEqual({
// start: { line: 1, column: 1, offset: 0 },
// end: { line: 2, column: 1, offset: 6 },
// })
// })
// it('could get by last char', () => {
// expect(c.linePosition(11)).toEqual({
// start: { line: 2, column: 1, offset: 6 },
// end: { line: 2, column: 7, offset: 12 },
// })
// })
// })
================================================
FILE: packages/text-kit/tests/lines.js
================================================
import assert from 'node:assert'
import { describe, it } from 'node:test'
import core from '../lib/core.js'
import lines from '../lib/utils/lines.js'
// TODO: add more tests
describe('linePosition', () => {
const testCases = [
{
desc: 'begin of line',
text: 'abcd',
point: { line: 1, column: 1, offset: 0 },
expected: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 5, offset: 4 }
}
},
{
desc: 'middle of line',
text: 'abcd',
point: { line: 1, column: 2, offset: 0 },
expected: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 5, offset: 4 }
}
},
{
desc: 'end of line',
text: 'abcd',
point: { line: 1, column: 4, offset: 0 },
expected: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 5, offset: 4 }
}
},
{
desc: 'out of bound (before)',
text: 'abcd',
point: { line: -1, column: 1, offset: -1 },
expected: null
},
{
desc: 'out of bound (after)',
text: 'abcd',
point: { line: 10, column: 1, offset: 100 },
expected: null
}
]
testCases.forEach(({ desc, text, point, expected }) => {
it(desc, () => {
const c = lines(core(text))
assert.deepStrictEqual(c.linePosition(point), expected)
})
})
})
================================================
FILE: packages/text-kit/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'packages/*'
- 'examples/*'
================================================
FILE: shell.nix
================================================
let
pkgs = import {};
nodejs = pkgs.nodejs_20;
pnpm = pkgs.pnpm;
in
pkgs.mkShell {
buildInputs = [ nodejs pnpm ];
}
================================================
FILE: tsconfig.base.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"composite": true,
"declarationMap": true,
"module": "commonjs",
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2020",
"sourceMap": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"declaration": true,
"jsx": "react",
"allowJs": true,
"lib": ["ES2020", "dom"]
},
"exclude": [
"node_modules",
"**/*.spec.ts",
"**/*.test.ts",
"**/__tests__/**",
"tests/**"
]
}
================================================
FILE: tsconfig.esm.json
================================================
{
"compilerOptions": {
"target": "ESNEXT",
"module": "ESNEXT",
"declaration": true,
"moduleResolution": "node",
"jsx": "react-jsx",
"skipLibCheck": true,
"esModuleInterop": true,
"allowJs": true,
"lib": ["ESNEXT", "DOM"],
"checkJs": true
},
"exclude": [
"node_modules",
"**/*.spec.ts",
"**/*.test.ts",
"**/__tests__/**",
"tests/**"
]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"checkJs": true,
"allowJs": true,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"exactOptionalPropertyTypes": true,
"jsx": "preserve",
"lib": ["esnext", "dom"],
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"strict": true,
"target": "esnext"
},
"exclude": [
"**/coverage/",
"**/node_modules/",
"**/tests/",
"**/dist/",
"./packages/reorg-parse/",
"./packages/reorg/",
"./packages/loader/"
],
"include": ["packages/**/*.js"]
}
================================================
FILE: tsconfig.packages.json
================================================
{
"files": [],
"references": [
{ "path": "packages/text-kit" },
{ "path": "packages/orga" },
{ "path": "packages/oast-to-hast" },
{ "path": "packages/reorg" },
{ "path": "packages/reorg-parse" },
{ "path": "packages/reorg-rehype" },
{ "path": "packages/next" },
{ "path": "packages/react" },
{ "path": "packages/metadata" },
{ "path": "packages/orgx" },
{ "path": "packages/rollup" }
]
}