integrate obsidian API and native CSS variables

- Update license to the current year
- Remove js-yaml dependency from core, switching to Obsidian's parseYaml API in the plugin
- Add CustomTheme support to dynamically map Obsidian's CSS color variables to the SVG renderer for better theme compatibility
- Extract hardcoded local vault path from esbuild config to a gitignored .env file
This commit is contained in:
waaraawa 2026-04-09 15:05:46 +09:00
parent 76c597622c
commit 880ca6bf93
9 changed files with 128 additions and 46 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 waaraawa
Copyright (c) 2026 waaraawa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -17,10 +17,8 @@
],
"author": "ByteGrid Contributors",
"license": "MIT",
"dependencies": {
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9"
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0"
}
}

View file

@ -3,28 +3,16 @@
* Parses YAML input to ByteGridConfig
*/
import * as yaml from 'js-yaml';
import { ByteGridConfig, Field, DataType } from './types';
import { ParseError } from './errors';
/**
* Parse YAML string to ByteGridConfig
* @param source YAML string
* @param parsed Parsed YAML object
* @returns Parsed ByteGridConfig
* @throws ParseError if parsing fails or required fields are missing
*/
export function parse(source: string): ByteGridConfig {
let parsed: unknown;
// Parse YAML
try {
parsed = yaml.load(source);
} catch (error) {
throw new ParseError(
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
);
}
export function parse(parsed: unknown): ByteGridConfig {
// Validate parsed result is an object
if (!parsed || typeof parsed !== 'object') {
throw new ParseError('Invalid YAML: expected an object');

View file

@ -3,7 +3,7 @@
* Renders LayoutBlocks to SVG string
*/
import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName, LegendPosition, ColorScheme } from './types';
import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName, LegendPosition, ColorScheme, CustomTheme } from './types';
/**
* Color palette mapping (default scheme)
@ -115,6 +115,12 @@ function rgbToHex(r: number, g: number, b: number): string {
* Transform color based on color scheme
*/
function transformColor(hexColor: string, scheme: ColorScheme): string {
// If the color is a CSS variable, we can't transform it via JS,
// we rely on the theme to provide the correct dark/light variants natively.
if (hexColor.startsWith('var(')) {
return hexColor;
}
if (scheme === 'default') {
return hexColor;
}
@ -150,6 +156,15 @@ const DEFAULT_OPTIONS: Required<RenderOptions> = {
cellHeight: 30,
fontSize: 10,
uniformRowHeight: false, // Bitfield rows are taller by default
theme: {
background: 'white',
textNormal: '#333',
textMuted: '#666',
textFaint: '#999',
border: '#333',
gridLine: '#ccc',
gridLineSubtle: '#ddd',
}
};
/**
@ -166,6 +181,15 @@ export function renderSVG(
options?: RenderOptions
): string {
const opts = { ...DEFAULT_OPTIONS, ...options };
const theme: CustomTheme = opts.theme || {
background: 'white',
textNormal: '#333',
textMuted: '#666',
textFaint: '#999',
border: '#333',
gridLine: '#ccc',
gridLineSubtle: '#ddd',
};
const layoutUnit = config.layoutUnit || 'byte';
const layout = config.layout || 16;
@ -338,10 +362,10 @@ export function renderSVG(
return block.bitfields && block.bitfields.length > 0 ? bitfieldCellHeight : normalCellHeight;
};
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" style="font-family: monospace; background: white;">`;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" style="font-family: monospace; background: ${theme.background};">`;
// Title
svg += `<text x="${totalWidth / 2}" y="30" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(config.name)}</text>`;
svg += `<text x="${totalWidth / 2}" y="30" text-anchor="middle" font-size="16" font-weight="bold" fill="${theme.textNormal}">${escapeXml(config.name)}</text>`;
// Column headers
if (opts.showGrid) {
@ -352,7 +376,7 @@ export function renderSVG(
const headerLabel = layoutUnit === 'bit'
? col % 8
: '0x' + col.toString(16).toUpperCase().padStart(2, '0');
svg += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="#666">${headerLabel}</text>`;
svg += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="${theme.textMuted}">${headerLabel}</text>`;
}
}
@ -366,7 +390,7 @@ export function renderSVG(
const startOffset = row * layout;
const hexOffset = '0x' + startOffset.toString(16).toUpperCase().padStart(2, '0');
const textY = y + cellHeight / 2 + 4;
svg += `<text x="${gridStartX - 5}" y="${textY}" text-anchor="end" font-size="9" fill="#666">${hexOffset}</text>`;
svg += `<text x="${gridStartX - 5}" y="${textY}" text-anchor="end" font-size="9" fill="${theme.textMuted}">${hexOffset}</text>`;
}
}
@ -376,11 +400,11 @@ export function renderSVG(
const y = rowYPositions.get(block.row) || gridStartY;
const width = block.span * opts.cellWidth;
const cellHeight = getCellHeight(block);
const baseColor = COLORS[block.color] || COLORS.gray;
const baseColor = theme.palette?.[block.color] || COLORS[block.color] || COLORS.gray;
const color = transformColor(baseColor, effectiveColorScheme);
// Draw block rectangle
svg += `<rect x="${x}" y="${y}" width="${width}" height="${cellHeight}" fill="${color}" stroke="#333" stroke-width="1"/>`;
svg += `<rect x="${x}" y="${y}" width="${width}" height="${cellHeight}" fill="${color}" stroke="${theme.border}" stroke-width="1"/>`;
// Draw numbers inside cells (only for byte layout)
if (layoutUnit !== 'bit') {
@ -389,7 +413,8 @@ export function renderSVG(
const hexNum = '0x' + byteNum.toString(16).toUpperCase().padStart(2, '0');
const cellX = x + i * opts.cellWidth + opts.cellWidth / 2;
const cellY = y + (cellHeight / 3); // Upper third for byte number
svg += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="#333">${hexNum}</text>`;
const textFill = theme.cellText || theme.textNormal;
svg += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="${textFill}">${hexNum}</text>`;
}
}
@ -402,14 +427,15 @@ export function renderSVG(
// Draw vertical lines dividing 8 bits
for (let bit = 1; bit < 8; bit++) {
const lineX = cellX + bit * bitCellWidth;
svg += `<line x1="${lineX}" y1="${y}" x2="${lineX}" y2="${y + cellHeight}" stroke="#ccc" stroke-width="0.5"/>`;
svg += `<line x1="${lineX}" y1="${y}" x2="${lineX}" y2="${y + cellHeight}" stroke="${theme.gridLine}" stroke-width="0.5"/>`;
}
// Draw bit numbers (0-7)
for (let bit = 0; bit < 8; bit++) {
const bitX = cellX + bit * bitCellWidth + bitCellWidth / 2;
const bitY = y + (cellHeight * 2 / 3) + 5; // Lower third for bit numbers
svg += `<text x="${bitX}" y="${bitY}" text-anchor="middle" font-size="7" fill="#666">${7 - bit}</text>`;
const bitTextFill = theme.cellTextMuted || theme.textMuted;
svg += `<text x="${bitX}" y="${bitY}" text-anchor="middle" font-size="7" fill="${bitTextFill}">${7 - bit}</text>`;
}
}
}
@ -422,9 +448,9 @@ export function renderSVG(
// Every 8 bits: darker dotted line (byte boundary)
// Other bits: lighter dotted line (bit boundary)
if (col % 8 === 0) {
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>`;
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="${theme.textFaint}" stroke-width="1" stroke-dasharray="3,3"/>`;
} else {
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#ddd" stroke-width="0.5" stroke-dasharray="2,2"/>`;
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="${theme.gridLineSubtle}" stroke-width="0.5" stroke-dasharray="2,2"/>`;
}
}
}
@ -451,7 +477,7 @@ export function renderSVG(
legendTitleY = legendStartY - 20;
}
svg += `<text x="${legendX}" y="${legendTitleY}" font-size="12" font-weight="bold" fill="#333">Fields</text>`;
svg += `<text x="${legendX}" y="${legendTitleY}" font-size="12" font-weight="bold" fill="${theme.textNormal}">Fields</text>`;
// Group blocks by field name to avoid duplicates
const uniqueFields = new Map<string, LayoutBlock>();
@ -501,17 +527,17 @@ export function renderSVG(
const x = legendX + col * legendColumnWidth;
const y = rowYPositions[row];
const baseColor = COLORS[block.color] || COLORS.gray;
const baseColor = theme.palette?.[block.color] || COLORS[block.color] || COLORS.gray;
const color = transformColor(baseColor, effectiveColorScheme);
// Color box
svg += `<rect x="${x}" y="${y}" width="20" height="20" fill="${color}" stroke="#333" stroke-width="1"/>`;
svg += `<rect x="${x}" y="${y}" width="20" height="20" fill="${color}" stroke="${theme.border}" stroke-width="1"/>`;
// Field name
svg += `<text x="${x + 30}" y="${y + 12}" font-size="11" font-weight="bold" fill="#333">${escapeXml(fieldName)}</text>`;
svg += `<text x="${x + 30}" y="${y + 12}" font-size="11" font-weight="bold" fill="${theme.textNormal}">${escapeXml(fieldName)}</text>`;
// Type
svg += `<text x="${x + 30}" y="${y + 24}" font-size="9" fill="#666">${escapeXml(block.fieldType)}</text>`;
svg += `<text x="${x + 30}" y="${y + 24}" font-size="9" fill="${theme.textMuted}">${escapeXml(block.fieldType)}</text>`;
// Offset info
const unit = layoutUnit === 'bit' ? 'bits' : 'bytes';
@ -523,15 +549,15 @@ export function renderSVG(
const offsetEndStr = layoutUnit === 'bit'
? block.offsetEnd.toString()
: '0x' + block.offsetEnd.toString(16).toUpperCase().padStart(2, '0');
svg += `<text x="${x + 30}" y="${y + 36}" font-size="9" fill="#999">offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})</text>`;
svg += `<text x="${x + 30}" y="${y + 36}" font-size="9" fill="${theme.textFaint}">offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})</text>`;
// Bitfields (if any)
if (block.bitfields && block.bitfields.length > 0) {
svg += `<text x="${x + 30}" y="${y + 48}" font-size="9" font-weight="bold" fill="#666">Bits:</text>`;
svg += `<text x="${x + 30}" y="${y + 48}" font-size="9" font-weight="bold" fill="${theme.textMuted}">Bits:</text>`;
block.bitfields.forEach((bf, bfIndex) => {
const bfY = y + 60 + bfIndex * 12;
svg += `<text x="${x + 35}" y="${bfY}" font-size="8" fill="#888">`;
svg += `<text x="${x + 35}" y="${bfY}" font-size="8" fill="${theme.textFaint}">`;
svg += `bit ${escapeXml(bf.bits)}: ${escapeXml(bf.name)}`;
svg += `</text>`;
});
@ -546,7 +572,7 @@ export function renderSVG(
const footerY = totalHeight - 40;
const sizeUnit = layoutUnit === 'bit' ? 'bits' : 'bytes';
const layoutUnitText = layoutUnit === 'bit' ? 'bits/row' : 'bytes/row';
svg += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="#999">Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}</text>`;
svg += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="${theme.textFaint}">Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}</text>`;
}
svg += `</svg>`;

View file

@ -132,6 +132,22 @@ export interface LayoutBlock {
bitfields?: Bitfield[];
}
/**
* Custom theme variables for rendering (e.g., for Obsidian integration)
*/
export interface CustomTheme {
background: string;
textNormal: string;
textMuted: string;
textFaint: string;
border: string;
gridLine: string;
gridLineSubtle: string;
cellText?: string;
cellTextMuted?: string;
palette?: Partial<Record<ColorName, string>>;
}
/**
* Rendering options
*/
@ -147,4 +163,5 @@ export interface RenderOptions {
cellHeight?: number;
fontSize?: number;
uniformRowHeight?: boolean; // If true, all rows have same height. If false (default), bitfield rows are taller
theme?: CustomTheme; // Custom theme overrides
}

View file

@ -3,9 +3,20 @@
* Tests for parsing YAML input to ByteGridConfig
*/
import { parse } from '../src/parser';
import * as yamlModule from 'js-yaml';
import { parse as coreParse } from '../src/parser';
import { ParseError } from '../src/errors';
function parse(source: string) {
try {
return coreParse(yamlModule.load(source));
} catch(error) {
throw new ParseError(
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
);
}
}
describe('Parser', () => {
describe('parse()', () => {
it('should parse valid YAML with minimal fields', () => {

View file

@ -0,0 +1,2 @@
# Configure your absolute path to the Obsidian plugin vault below for automatic copying on build
# OBSIDIAN_PLUGIN_DIR=/path/to/your/vault/.obsidian/plugins/bytegrid

View file

@ -14,7 +14,16 @@ const prod = process.argv[2] === 'production';
const watch = process.argv[2] === '--watch';
// Obsidian vault plugin directory
const OBSIDIAN_PLUGIN_DIR = '/Users/jinjung/Dropbox/obsidian/.obsidian/plugins/bytegrid';
let OBSIDIAN_PLUGIN_DIR = process.env.OBSIDIAN_PLUGIN_DIR;
try {
const envContent = fs.readFileSync('.env', 'utf8');
const match = envContent.match(/^OBSIDIAN_PLUGIN_DIR=(.*)$/m);
if (match) {
OBSIDIAN_PLUGIN_DIR = match[1].trim();
}
} catch (e) {
// Ignore if .env doesn't exist
}
// Plugin to copy files to Obsidian after build
const copyPlugin = {
@ -27,6 +36,11 @@ const copyPlugin = {
}
try {
if (!OBSIDIAN_PLUGIN_DIR) {
console.log(' OBSIDIAN_PLUGIN_DIR is not set, skipping copy to Obsidian vault');
return;
}
// Ensure directory exists
if (!fs.existsSync(OBSIDIAN_PLUGIN_DIR)) {
fs.mkdirSync(OBSIDIAN_PLUGIN_DIR, { recursive: true });

View file

@ -1,4 +1,4 @@
import { Plugin } from 'obsidian';
import { Plugin, parseYaml } from 'obsidian';
import { parse, validate, createLayout, renderSVG } from '@bytegrid/core';
export default class ByteGridPlugin extends Plugin {
@ -6,7 +6,8 @@ export default class ByteGridPlugin extends Plugin {
this.registerMarkdownCodeBlockProcessor('bytegrid', (source, el, ctx) => {
try {
// Parse YAML input
const config = parse(source);
const parsed = parseYaml(source);
const config = parse(parsed);
// Validate configuration
validate(config);
@ -14,8 +15,33 @@ export default class ByteGridPlugin extends Plugin {
// Create layout blocks
const blocks = createLayout(config);
// Render to SVG
const svg = renderSVG(config, blocks);
// Render to SVG integrating with Obsidian's CSS variables
const svg = renderSVG(config, blocks, {
theme: {
background: 'transparent',
textNormal: 'var(--text-normal)',
textMuted: 'var(--text-muted)',
textFaint: 'var(--text-faint)',
border: 'var(--background-modifier-border)',
gridLine: 'var(--background-modifier-border)',
gridLineSubtle: 'var(--background-modifier-border-hover)',
// For block cell text, using var(--text-normal) might lack contrast against colored blocks.
// We use a high-contrast fallback variable or default to white/black via CSS if available.
// Obsidian's callouts use --callout-title-color which might be good, but for now we rely on standard text-muted.
// Let's omit cellText overrides to fallback to textNormal/textMuted,
palette: {
blue: 'var(--color-blue)',
cyan: 'var(--color-cyan)',
yellow: 'var(--color-yellow)',
green: 'var(--color-green)',
orange: 'var(--color-orange)',
purple: 'var(--color-purple)',
mint: 'var(--color-green)', // No mint in standard Obsidian, fallback to green
pink: 'var(--color-pink)',
gray: 'var(--color-base-40)',
}
}
});
// Display SVG safely using DOMParser
const parser = new DOMParser();