Skip to content

Commit

Permalink
Merge pull request #36 from 0918nobita/v0.4.0-code-block-processor
Browse files Browse the repository at this point in the history
レイアウト計算をメモ化して効率化されたレンダラを実装した
  • Loading branch information
0918nobita committed Dec 27, 2023
2 parents 1e054b2 + 5a56dce commit 27d9f16
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 142 deletions.
18 changes: 6 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,12 @@

### Update dependencies

| package | from | to |
| -------------------------------- | ------- | ------- |
| @types/node | 20.10.0 | 20.10.4 |
| @typescript-eslint/eslint-plugin | 6.13.0 | 6.15.0 |
| @typescript-eslint/parser | 6.13.0 | 6.15.0 |
| @vanilla-extract/vite-plugin | 3.9.2 | 3.9.3 |
| eslint | 8.54.0 | 8.56.0 |
| globals | 13.23.0 | 13.24.0 |
| pinyin-pro | 3.18.2 | 3.18.5 |
| prettier | 3.1.0 | 3.1.1 |
| typescript | 5.3.2 | 5.3.3 |
| vite | 4.5.0 | 4.5.1 |
| package | from | to |
| ---------------------------- | ------ | ------ |
| @vanilla-extract/vite-plugin | 3.9.2 | 3.9.3 |
| pinyin-pro | 3.18.2 | 3.18.5 |
| typescript | 5.3.2 | 5.3.3 |
| vite | 4.5.0 | 4.5.1 |

## [0.3.1](https://github.com/0918nobita/obsidian-zhongwen-block/compare/0.3.0...0.3.1) (2023-11-28)

Expand Down
1 change: 1 addition & 0 deletions src/branded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Branded<T, Brand> = T & { __brand: Brand };
81 changes: 0 additions & 81 deletions src/code-block-processor.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/code-block-processor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { codeBlockProcessor } from './render';
export type { LayoutMemo } from './memo';
22 changes: 22 additions & 0 deletions src/code-block-processor/measure-width.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const waitAndMeasureCorrectWidth = async (
element: HTMLElement,
): Promise<number> => {
const { width } = element.getBoundingClientRect();
if (width !== 0) return width;

return new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
if (entries.length === 0) {
return;
}

const { width } = entries[0].boundingClientRect;
if (width !== 0) {
observer.disconnect();
resolve(width);
}
});

observer.observe(element);
});
};
8 changes: 8 additions & 0 deletions src/code-block-processor/memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ZhCharBlockLayout {
zhChar: string;
zhCharWidth: number;
pinyin: string;
pinyinWidth: number;
}

export type LayoutMemo = Map<string, ZhCharBlockLayout[]>;
223 changes: 223 additions & 0 deletions src/code-block-processor/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import * as PinyinPro from 'pinyin-pro';

import { waitAndMeasureCorrectWidth } from './measure-width';
import type { LayoutMemo, ZhCharBlockLayout } from './memo';
import {
NonZhSegment,
ZhSegment,
splitSentenceIntoSegments,
} from './split-sentence';
import type { Branded } from '../branded';
import type { Settings } from '../settings';

import * as styles from './style.css';

type PinyinLine = Branded<HTMLDivElement, 'pinyinLine'>;

type HiddenZhChar = Branded<HTMLSpanElement, 'hiddenZhChar'>;

type PinyinSpan = Branded<HTMLSpanElement, 'pinyinSpan'>;

type ZhCharLine = Branded<HTMLDivElement, 'zhCharLine'>;

const createHiddenZhBlock = (
pinyinLine: PinyinLine,
{
zhChar,
pinyin,
pinyinSpanClass,
}: { zhChar: string; pinyin: string; pinyinSpanClass: string },
): { hiddenZhChar: HiddenZhChar; pinyinSpan: PinyinSpan } => {
const hiddenZhBlock = pinyinLine.createDiv({
cls: styles.hiddenZhBlock,
});

const hiddenZhChar = hiddenZhBlock.createSpan({
cls: styles.hiddenZhChar,
text: zhChar,
}) as HiddenZhChar;

const pinyinSpan = hiddenZhBlock.createSpan({
cls: pinyinSpanClass,
text: pinyin,
}) as PinyinSpan;

return { hiddenZhChar, pinyinSpan };
};

const gap = 2;

const computePadding = ({
zhCharWidth,
pinyinWidth,
}: {
zhCharWidth: number;
pinyinWidth: number;
}): { zhCharPadding: number; pinyinPadding: number } =>
pinyinWidth >= zhCharWidth
? {
pinyinPadding: gap,
zhCharPadding: (pinyinWidth - zhCharWidth) / 2 + gap,
}
: {
pinyinPadding: (zhCharWidth - pinyinWidth) / 2 + gap,
zhCharPadding: gap,
};

const createVisibleZhBlock = (
zhCharLine: ZhCharLine,
zhChar: string,
zhCharPadding: number,
): void => {
const visibleZhBlock = zhCharLine.createDiv({
cls: styles.visibleZhBlock,
text: zhChar,
});
visibleZhBlock.style.paddingLeft = `${zhCharPadding}px`;
visibleZhBlock.style.paddingRight = `${zhCharPadding}px`;
};

const renderNonZhSegment = (
segment: NonZhSegment,
{
pinyinLine,
zhCharLine,
}: { pinyinLine: PinyinLine; zhCharLine: ZhCharLine },
): void => {
pinyinLine.createSpan({
cls: styles.hiddenNonZh,
text: segment.nonZhChars,
});

zhCharLine.appendText(segment.nonZhChars);
};

const renderAndMemorizeZhSegment = async (
segment: ZhSegment,
{
pinyinLine,
zhCharLine,
}: { pinyinLine: PinyinLine; zhCharLine: ZhCharLine },
pinyinSpanClass: string,
layoutMemo: LayoutMemo,
): Promise<void> => {
const zhCharBlocks: ZhCharBlockLayout[] = [];

const pinyinData = PinyinPro.pinyin(segment.zhChars, {
type: 'all',
});

for (const { pinyin, origin: zhChar } of pinyinData) {
const { hiddenZhChar, pinyinSpan } = createHiddenZhBlock(pinyinLine, {
zhChar,
pinyin,
pinyinSpanClass,
});

const zhCharWidth = await waitAndMeasureCorrectWidth(hiddenZhChar);

const pinyinWidth = await waitAndMeasureCorrectWidth(pinyinSpan);

const { zhCharPadding, pinyinPadding } = computePadding({
zhCharWidth,
pinyinWidth,
});

hiddenZhChar.style.paddingLeft = `${zhCharPadding}px`;
hiddenZhChar.style.paddingRight = `${zhCharPadding}px`;

pinyinSpan.style.paddingLeft = `${pinyinPadding}px`;
pinyinSpan.style.paddingRight = `${pinyinPadding}px`;

createVisibleZhBlock(zhCharLine, zhChar, zhCharPadding);

zhCharBlocks.push({
zhChar,
zhCharWidth,
pinyin,
pinyinWidth,
});
}

layoutMemo.set(segment.zhChars, zhCharBlocks);
};

const renderMemorizedZhSegment = (
zhCharBlocks: ZhCharBlockLayout[],
{
pinyinLine,
zhCharLine,
}: { pinyinLine: PinyinLine; zhCharLine: ZhCharLine },
pinyinSpanClass: string,
): void => {
for (const { zhChar, zhCharWidth, pinyin, pinyinWidth } of zhCharBlocks) {
const { hiddenZhChar, pinyinSpan } = createHiddenZhBlock(pinyinLine, {
zhChar,
pinyin,
pinyinSpanClass,
});

const { zhCharPadding, pinyinPadding } = computePadding({
zhCharWidth,
pinyinWidth,
});

hiddenZhChar.style.paddingLeft = `${zhCharPadding}px`;
hiddenZhChar.style.paddingRight = `${zhCharPadding}px`;

pinyinSpan.style.paddingLeft = `${pinyinPadding}px`;
pinyinSpan.style.paddingRight = `${pinyinPadding}px`;

createVisibleZhBlock(zhCharLine, zhChar, zhCharPadding);
}
};

export const codeBlockProcessor = async (
source: string,
element: HTMLElement,
settings: Settings,
layoutMemo: LayoutMemo,
): Promise<void> => {
const pinyinSpanClass = settings.alwaysDisplayPinyin
? styles.pinyin.base
: `${styles.pinyin.base} ${styles.pinyin.displayOnHover}`;

const container = element.createDiv({
cls: styles.container,
});

const pinyinLine = container.createDiv({
cls: styles.pinyinLine,
}) as PinyinLine;

const zhCharLine = container.createDiv({
cls: styles.zhCharLine,
}) as ZhCharLine;

const segments = splitSentenceIntoSegments(source.trim());

for (const segment of segments) {
if (segment.type === 'nonZh') {
renderNonZhSegment(segment, { pinyinLine, zhCharLine });
continue;
}

const zhCharBlocks = layoutMemo.get(segment.zhChars);

if (zhCharBlocks === undefined) {
await renderAndMemorizeZhSegment(
segment,
{ pinyinLine, zhCharLine },
pinyinSpanClass,
layoutMemo,
);
continue;
}

renderMemorizedZhSegment(
zhCharBlocks,
{ pinyinLine, zhCharLine },
pinyinSpanClass,
);
}
};
Loading

0 comments on commit 27d9f16

Please sign in to comment.