Documentation Index
Fetch the complete documentation index at: https://mintlify.com/inkdown/inkdown/llms.txt
Use this file to discover all available pages before exploring further.
Inkdown follows strict coding conventions to maintain code quality and consistency across the codebase.
We use Biome for linting and formatting. Configuration is in biome.json at the project root.
Running Biome
# Check for issues
bun run lint
# Fix issues automatically
bun run lint:fix
# Format code
bun run format
# Run all checks (recommended before commit)
bun run check
# Fix all issues
bun run check:fix
Biome Configuration
Key settings from biome.json:
{
"formatter": {
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 100,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always"
}
}
}
Summary:
- 4 spaces for indentation (not tabs)
- 100 character line width
- Single quotes for JavaScript/TypeScript
- Double quotes for JSX attributes
- Always use semicolons
- Always use trailing commas
- Always use parentheses around arrow function parameters
TypeScript
Strict Mode
Always use TypeScript strict mode:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
Type Safety
DO:
- Use explicit types for function parameters and return values
- Use
unknown instead of any when type is truly unknown
- Use type guards to narrow types
- Export types from their defining module
// Good ✓
function processFile(path: string): Promise<string> {
return readFile(path);
}
function parseData(data: unknown): User {
if (!isUserData(data)) {
throw new Error('Invalid user data');
}
return data;
}
// Bad ✗
function processFile(path) {
return readFile(path);
}
function parseData(data: any): User {
return data;
}
Interfaces vs Types
- Use interfaces for object shapes
- Use type aliases for unions, primitives, and utility types
// Interfaces for objects
interface User {
id: string;
name: string;
email: string;
}
// Types for unions and utilities
type Status = 'active' | 'inactive' | 'pending';
type Readonly<T> = { readonly [K in keyof T]: T[K] };
Enums
Use string enums with initializers:
enum ViewMode {
Edit = 'edit',
Preview = 'preview',
SideBySide = 'side-by-side',
}
File Organization
File Structure
packages/core/src/
├── managers/ # Core managers
│ ├── CommandManager.ts
│ └── ThemeManager.ts
├── types/ # Type definitions
│ └── config.ts
├── utils/ # Utility functions
│ └── logger.ts
└── index.ts # Public exports
Naming Conventions
-
Files: PascalCase for classes, camelCase for utilities
ThemeManager.ts (class)
logger.ts (utility)
index.ts (barrel export)
-
Classes: PascalCase
-
Interfaces: PascalCase with
I prefix for implementations
interface IThemeProvider { }
interface ThemeConfig { } // No prefix for data types
-
Functions: camelCase
function parseMarkdown() { }
-
Constants: UPPER_SNAKE_CASE for true constants
const DEFAULT_THEME = 'light';
const MAX_FILE_SIZE = 10 * 1024 * 1024;
-
Variables: camelCase
const userName = 'John';
let isActive = true;
Imports
Import order:
- External packages
- Internal packages (from
@inkdown/)
- Relative imports (same package)
- Type imports (at the end)
// External packages
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
// Internal packages
import { Plugin } from '@inkdown/core';
import { Modal } from '@inkdown/core/ui';
// Relative imports
import { parseConfig } from './utils';
import { DEFAULT_CONFIG } from './constants';
// Type imports
import type { Config } from './types';
import type { ThemeConfig } from '@inkdown/core';
Use absolute imports for cross-package imports:
// Good ✓
import { App } from '@inkdown/core';
// Bad ✗
import { App } from '../../../core/src/App';
Styling
CSS Files
ALWAYS use external CSS files, never inline styles:
// Good ✓
import './MyComponent.css';
function MyComponent() {
return <div className="my-component">Content</div>;
}
/* MyComponent.css */
.my-component {
padding: var(--spacing-md);
background: var(--bg-primary);
}
CSS Variables
ALWAYS use CSS variables for colors and theme values:
/* Good ✓ */
.button {
background: var(--color-primary);
color: var(--text-primary);
border: 1px solid var(--border-primary);
padding: var(--spacing-sm);
}
/* Bad ✗ */
.button {
background: #6c99bb;
color: #dcdcdc;
border: 1px solid #444;
padding: 8px;
}
CSS Organization
/* Component base styles */
.component {
/* Layout */
display: flex;
flex-direction: column;
/* Spacing */
padding: var(--spacing-md);
margin: 0;
/* Colors */
background: var(--bg-primary);
color: var(--text-primary);
/* Typography */
font-size: var(--font-size-base);
line-height: 1.5;
}
/* Component states */
.component:hover {
background: var(--bg-secondary);
}
.component.active {
border-color: var(--color-primary);
}
DO comment:
- Complex algorithms or logic
- Non-obvious workarounds
- Public APIs (JSDoc)
- Type definitions
DON’T comment:
- Obvious code
- What the code does (code should be self-documenting)
// Good ✓
/**
* Parses frontmatter from markdown content
* @param content - The markdown content
* @returns Parsed frontmatter object
*/
function parseFrontmatter(content: string): Record<string, unknown> {
// Use regex to extract YAML frontmatter between --- markers
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
return parseYaml(match[1]);
}
// Bad ✗
// This function parses frontmatter
function parseFrontmatter(content: string) {
// Match the content
const match = content.match(/^---\n([\s\S]*?)\n---/);
// If no match, return empty object
if (!match) return {};
// Parse the YAML
return parseYaml(match[1]);
}
JSDoc
Use JSDoc for public APIs:
/**
* Registers a new command
* @param command - Command configuration
* @returns The registered command ID
* @example
* ```typescript
* app.commandManager.register({
* id: 'my-command',
* name: 'My Command',
* callback: () => console.log('Hello'),
* });
* ```
*/
register(command: Command): string {
// Implementation
}
Code Organization
Class Structure
export class ThemeManager {
// 1. Static properties
private static instance: ThemeManager;
// 2. Instance properties
private app: App;
private currentTheme: string;
private themes: Map<string, Theme>;
// 3. Constructor
constructor(app: App) {
this.app = app;
this.themes = new Map();
}
// 4. Public methods
async setTheme(themeId: string): Promise<void> {
// Implementation
}
getTheme(themeId: string): Theme | undefined {
return this.themes.get(themeId);
}
// 5. Private methods
private loadTheme(themeId: string): Promise<Theme> {
// Implementation
}
private applyThemeStyles(theme: Theme): void {
// Implementation
}
}
Function Length
Keep functions focused and small:
- Maximum ~50 lines per function
- Extract complex logic into helper functions
- One function, one purpose
// Good ✓
function processFile(path: string): Promise<void> {
const content = await readFile(path);
const parsed = parseContent(content);
const validated = validateData(parsed);
await saveData(validated);
}
// Bad ✗ (too long, doing too much)
function processFile(path: string): Promise<void> {
// 100+ lines of mixed responsibilities
}
Never access platform-specific APIs directly in core code:
// Good ✓ - Use abstraction
import { NativeBridge } from '@inkdown/core/native';
const content = await NativeBridge.fs.readFile(path);
// Bad ✗ - Direct platform access
import { readFile } from 'fs/promises';
const content = await readFile(path);
UI Abstraction
Always use UIBridge for UI operations:
// Good ✓
import { UIBridge } from '@inkdown/core/ui';
UIBridge.showNotice('File saved!');
// Bad ✗
document.createElement('div');
Error Handling
Result Type
Use the Result type for operations that can fail:
import { Result } from '@inkdown/core/errors';
function readConfig(path: string): Result<Config, Error> {
try {
const content = readFile(path);
const config = JSON.parse(content);
return Result.ok(config);
} catch (error) {
return Result.err(new Error(`Failed to read config: ${error}`));
}
}
// Usage
const result = readConfig('config.json');
if (result.isOk()) {
console.log('Config:', result.value);
} else {
console.error('Error:', result.error);
}
AppError
Use AppError for application-specific errors:
import { AppError } from '@inkdown/core/errors';
throw new AppError(
'FILE_NOT_FOUND',
`File not found: ${path}`,
{ path }
);
Memoization
Memoize expensive computations:
import { useMemo } from 'react';
const sortedFiles = useMemo(() => {
return files.sort((a, b) => a.name.localeCompare(b.name));
}, [files]);
Debouncing
Debounce frequent operations:
const debouncedSearch = debounce((query: string) => {
performSearch(query);
}, 300);
Testing
See Testing Guide for detailed testing practices.
Linting Rules
Key Rules
- No unused variables/imports (warning)
- No
any type (disabled, use unknown)
- No
console.log in production code (off in dev)
- Exhaustive dependency arrays for React hooks
- No implicit boolean attributes
Overrides
Test files have relaxed rules:
{
"overrides": [
{
"includes": ["*.test.ts", "*.test.tsx"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
Pre-commit Checklist
Before committing, ensure:
# 1. Format and lint
bun run check:fix
# 2. Type check
bun run typecheck
# 3. Run tests
bun run test:unit
# 4. Verify build (optional)
bun run build
Summary
- Use Biome for formatting and linting
- Follow TypeScript strict mode
- Use CSS files with CSS variables for styling
- Write self-documenting code with JSDoc for public APIs
- Use platform abstractions (NativeBridge, UIBridge)
- Keep functions small and focused
- Handle errors with Result type or AppError
- Always run
bun run check before committing
Next: Testing Guide