fork extracting Logseq's frontend editor for personal use
A powerful outliner-based markdown editor library with bi-directional linking, tags, and full markdown support. Built with React, TypeScript, and the mldoc parser.
- Block-based Hierarchical Structure - Organize content in nested blocks like Logseq and Workflowy
- Full Markdown Support - Complete markdown syntax powered by the mldoc parser
- Bi-directional Links - Page references
[[page]]and block references((uuid)) - Tag Support - Organize with
#tagsand#[[multi-word tags]] - Inline Editing - Seamless editing experience with keyboard shortcuts
- Collapse/Expand - Hierarchical navigation with collapsible blocks
- Pluggable Storage - Swap storage backends (memory, IndexedDB, etc.)
- Type-safe - Full TypeScript support with comprehensive type definitions
- Renderer Components - Display markdown content with interactive elements
- Dark Mode Ready - Built-in dark mode support with Tailwind CSS
npm install live-quick-markOr with yarn:
yarn add live-quick-markOr with pnpm:
pnpm add live-quick-markimport { Outliner, useBlockStore, createMemoryAdapter } from 'live-quick-mark';
import 'live-quick-mark/styles';
function MyApp() {
const { setAdapter, createPage, currentPageId } = useBlockStore();
useEffect(() => {
// Initialize storage adapter
const adapter = createMemoryAdapter();
setAdapter(adapter);
// Create a page
createPage('My First Page').then(page => {
setCurrentPage(page.id);
});
}, []);
return (
<div>
{currentPageId && (
<Outliner pageId={currentPageId} mode="edit" />
)}
</div>
);
}import { Renderer } from 'live-quick-mark';
function MarkdownViewer() {
return (
<Renderer
content="# Hello [[World]]\n\nThis is **bold** text with a #tag"
onPageClick={(page) => console.log('Navigate to:', page)}
onTagClick={(tag) => console.log('Filter by:', tag)}
/>
);
}import { BlockEditor } from 'live-quick-mark';
function MyBlockEditor({ block }) {
return (
<BlockEditor
block={block}
autoFocus
onCreateBlock={(afterId) => console.log('Create block after:', afterId)}
onDeleteBlock={(id) => console.log('Delete block:', id)}
onIndent={(id) => console.log('Indent:', id)}
onOutdent={(id) => console.log('Outdent:', id)}
/>
);
}Displays a hierarchical tree of blocks with editing capabilities.
Props:
pageId: string- The page ID to display blocks forclassName?: string- Optional CSS class namemode?: 'read' | 'edit'- Display mode (default: 'edit')
Example:
<Outliner pageId="page-123" mode="edit" />Inline editor for a single block with keyboard shortcuts.
Props:
block: Block- The block to editautoFocus?: boolean- Whether to auto-focus on mountonCreateBlock?: (afterBlockId: string) => void- Callback for Enter keyonDeleteBlock?: (blockId: string) => void- Callback for Backspace on emptyonIndent?: (blockId: string) => void- Callback for Tab keyonOutdent?: (blockId: string) => void- Callback for Shift+TabonFocusPrevious?: (blockId: string) => void- Callback for ArrowUponFocusNext?: (blockId: string) => void- Callback for ArrowDownclassName?: string- Optional CSS class name
Keyboard Shortcuts:
Enter- Create new block belowBackspace(on empty) - Delete blockTab- Indent blockShift+Tab- Outdent blockArrowUp(at start) - Focus previous blockArrowDown(at end) - Focus next block
Renders markdown content to React elements.
Props:
content: string- The markdown content to renderconfig?: Partial<ParserConfig>- Parser configurationonPageClick?: (pageName: string) => void- Callback for page reference clicksonBlockClick?: (blockId: string) => void- Callback for block reference clicksonTagClick?: (tag: string) => void- Callback for tag clicksonCheckboxChange?: (checked: boolean, itemIndex: number[]) => void- Callback for checkbox changesclassName?: string- Optional CSS class namestyle?: React.CSSProperties- Optional inline styles
Example:
<Renderer
content="# Hello [[World]]\n- [ ] Task"
onPageClick={(page) => navigateTo(page)}
onCheckboxChange={(checked, path) => updateTask(path, checked)}
/>Zustand store hook for managing blocks and pages.
State:
blocks: Map<string, Block>- All blockspages: Map<string, Page>- All pagescurrentPageId: string | null- Currently active pagezoomedBlockId: string | null- Currently zoomed blockfocusedBlockId: string | null- Currently focused blockselectedBlockIds: Set<string>- Currently selected blocks
Actions:
Block Operations:
createBlock(block: Partial<Block>): Promise<Block>- Create a new blockupdateBlock(id: string, updates: Partial<Block>): Promise<void>- Update a blockdeleteBlock(id: string): Promise<void>- Delete a blockmoveBlock(blockId: string, newParentId: string | null, newOrder: number): Promise<void>- Move a blockindentBlock(blockId: string): Promise<void>- Indent a block (make it child of previous sibling)outdentBlock(blockId: string): Promise<void>- Outdent a block (move to parent's level)toggleCollapse(blockId: string): Promise<void>- Toggle block collapse state
Page Operations:
createPage(name: string, properties?: Record<string, unknown>): Promise<Page>- Create a new pageupdatePage(id: string, updates: Partial<Page>): Promise<void>- Update a pagedeletePage(id: string): Promise<void>- Delete a page
Navigation:
setCurrentPage(pageId: string | null): void- Set the current pagezoomToBlock(blockId: string | null): void- Zoom into a blockfocusBlock(blockId: string | null): void- Focus a blockselectBlocks(blockIds: string[]): void- Select multiple blocksclearSelection(): void- Clear block selection
Queries:
getBlock(id: string): Block | null- Get a block by IDgetPage(id: string): Page | null- Get a page by IDgetPageBlocks(pageId: string): Block[]- Get all blocks for a pagegetChildren(blockId: string): Block[]- Get child blocksgetParent(blockId: string): Block | null- Get parent blockgetSiblings(blockId: string): Block[]- Get sibling blocks
Example:
const { createBlock, updateBlock, focusBlock } = useBlockStore();
// Create a block
const block = await createBlock({
pageId: 'page-123',
content: 'Hello world',
parentId: null,
order: 0,
});
// Update it
await updateBlock(block.id, {
content: 'Updated content'
});
// Focus it
focusBlock(block.id);Creates an in-memory storage adapter for development and testing.
Example:
const adapter = createMemoryAdapter();
useBlockStore.getState().setAdapter(adapter);interface Block {
id: string;
content: string;
pageId?: string;
parentId: string | null;
children: string[];
collapsed: boolean;
createdAt: string;
updatedAt: string;
order?: number;
properties?: Record<string, unknown>;
}interface Page {
id: string;
name: string;
blockIds: string[];
properties?: Record<string, unknown>;
createdAt: number;
updatedAt: number;
}interface StorageAdapter {
getBlock(id: string): Promise<Block | null>;
saveBlock(block: Block): Promise<void>;
deleteBlock(id: string): Promise<void>;
getPage(id: string): Promise<Page | null>;
getAllPages(): Promise<Page[]>;
savePage(page: Page): Promise<void>;
deletePage(id: string): Promise<void>;
getBlocks(ids: string[]): Promise<Map<string, Block>>;
saveBlocks(blocks: Block[]): Promise<void>;
getPageBlocks(pageId: string): Promise<Block[]>;
getChildBlocks(parentId: string): Promise<Block[]>;
}// Page references
import { isPageRef, createPageRef, getPageName, extractAllPageRefs } from 'live-quick-mark';
isPageRef('[[My Page]]'); // true
createPageRef('My Page'); // '[[My Page]]'
getPageName('[[My Page]]'); // 'My Page'
extractAllPageRefs('See [[Page 1]] and [[Page 2]]'); // ['Page 1', 'Page 2']
// Block references
import { isBlockRef, createBlockRef, getBlockRefId, extractAllBlockRefs } from 'live-quick-mark';
// Tags
import { isTag, createTag, getTagName, extractAllTags } from 'live-quick-mark';
// Real-time detection
import { detectTrigger } from 'live-quick-mark';
const detection = detectTrigger('Hello [[wor', 9); // Position at 'r'
if (detection) {
console.log(detection.type); // 'page-ref'
console.log(detection.query); // 'wor'
console.log(detection.range); // { start: 6, end: 11 }
}import { parseContent, parseBlock, getDefaultConfig } from 'live-quick-mark';
// Parse multiple blocks
const blocks = parseContent('# Heading\n\nParagraph');
// Parse single block
const block = parseBlock('**Bold** text');
// Get default config
const config = getDefaultConfig();# Clone the repository
git clone https://github.com/logseq/logseq.git
cd logseq
# Install dependencies
npm install
# Start the dev server
npm run devVisit http://localhost:5173 to see the demo app.
# Build for production
npm run build:lib
# The built files will be in the dist/ directorysrc/
├── components/ # React components
│ ├── Outliner.tsx # Main outliner component
│ └── BlockEditor.tsx # Block editor component
├── renderer/ # Markdown renderer
│ ├── index.tsx # Main renderer
│ ├── BlockRenderer.tsx
│ └── InlineRenderer.tsx
├── store/ # Zustand store
│ ├── store.ts # Store implementation
│ ├── types.ts # Store types
│ └── memory-adapter.ts
├── parser/ # mldoc parser wrapper
│ ├── index.ts
│ └── config.ts
├── links/ # Link detection utilities
│ ├── page-ref.ts
│ ├── block-ref.ts
│ ├── tag.ts
│ └── detect.ts
├── types/ # TypeScript types
│ ├── ast.ts # AST types
│ ├── block.ts # Block types
│ └── index.ts
├── App.tsx # Demo application
├── main.tsx # Demo entry point
└── index.ts # Library entry point
Implement your own storage backend:
import type { StorageAdapter, Block, Page } from 'live-quick-mark';
class IndexedDBAdapter implements StorageAdapter {
async getBlock(id: string): Promise<Block | null> {
// Your implementation
}
async saveBlock(block: Block): Promise<void> {
// Your implementation
}
// ... implement other methods
}
// Use it
const adapter = new IndexedDBAdapter();
useBlockStore.getState().setAdapter(adapter);import { Renderer, getDefaultConfig } from 'live-quick-mark';
const customConfig = {
...getDefaultConfig(),
format: 'markdown' as const,
parseHeading: true,
parseList: true,
// ... other options
};
<Renderer content={markdown} config={customConfig} />Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
MIT License - see LICENSE file for details
Built with:
- React - UI framework
- TypeScript - Type safety
- Zustand - State management
- mldoc - Markdown/Org parser
- Tailwind CSS - Styling
- Vite - Build tool
Inspired by:
- Logseq - Outliner-based note-taking
- Workflowy - Simple outlining
- Roam Research - Bi-directional linking
- Documentation: [Link to docs]
- Issues: GitHub Issues
- Discussions: GitHub Discussions