λ°μ½λ μ΄μ (Decorations)μ μ¬μ©νλ©΄ μλν° νμ₯ κΈ°λ₯μμ μ½ν μΈ λ₯Ό μ΄λ»κ² 그리거λ μ€νμΌλ§ν μ§ μ μ΄ν μ μμ΅λλ€. μλν°μμ μμλ₯Ό μΆκ°, κ΅μ²΄νκ±°λ μ€νμΌμ λ³κ²½νμ¬ λͺ¨μκ³Ό λλμ λ°κΎΈλ €λ©΄ λλΆλΆ λ°μ½λ μ΄μ μ μ¬μ©ν΄μΌ ν©λλ€.
μ΄ νμ΄μ§λ₯Ό μ½κ³ λλ©΄ λ€μμ ν μ μκ² λ©λλ€:
- μλν° μΈκ΄μ λ³κ²½νκΈ° μν΄ λ°μ½λ μ΄μ μ μ¬μ©νλ λ°©λ² μ΄ν΄
- μν νλμ λ·° νλ¬κ·ΈμΈμ μ¬μ©νμ¬ λ°μ½λ μ΄μ μ μ 곡νλ μ°¨μ΄μ μ΄ν΄
Note
μ΄ νμ΄μ§λ Obsidian νλ¬κ·ΈμΈ κ°λ°μλ₯Ό μν΄ κ³΅μ CodeMirror 6 λ¬Έμλ₯Ό μμ½ν κ²μ λλ€. μν νλμ λν λ μμΈν μ 보λ Decorating the Documentλ₯Ό μ°Έμ‘°νμΈμ.
νμ 쑰건
- State fields κΈ°λ³Έ μ΄ν΄
- View plugins κΈ°λ³Έ μ΄ν΄
κ°μ
λ°μ½λ μ΄μ μμ΄λ λ¬Έμκ° μΌλ° ν μ€νΈλ‘ λ λλ§λ©λλ€. μ ν ν₯λ―Έλ‘μ§ μμ£ . λ°μ½λ μ΄μ μ μ¬μ©νλ©΄ ν μ€νΈ κ°μ‘°λ 컀μ€ν HTML μμ μΆκ°μ κ°μ΄ λ¬Έμ νμ λ°©μμ λ³κ²½ν μ μμ΅λλ€.
λ€μ μ νμ λ°μ½λ μ΄μ μ μ¬μ©ν μ μμ΅λλ€:
- λ§ν¬ λ°μ½λ μ΄μ : κΈ°μ‘΄ μμ μ€νμΌλ§
- μμ ― λ°μ½λ μ΄μ : λ¬Έμμ μμ μ½μ
- κ΅μ²΄ λ°μ½λ μ΄μ : λ¬Έμ μΌλΆλ₯Ό λ€λ₯Έ μμλ‘ μ¨κΈ°κ±°λ κ΅μ²΄
- λΌμΈ λ°μ½λ μ΄μ : λ¬Έμ μμ²΄κ° μλ λΌμΈμ μ€νμΌ μΆκ°
λ°μ½λ μ΄μ μ μ¬μ©νλ €λ©΄ μλν° νμ₯ κΈ°λ₯ λ΄λΆμμ λ°μ½λ μ΄μ μ μμ±νκ³ νμ₯ κΈ°λ₯μ΄ μ΄λ₯Ό μλν°μ _μ 곡_νλλ‘ ν΄μΌ ν©λλ€. λ°μ½λ μ΄μ μ μλν°μ μ 곡νλ λ°©λ²μ λ κ°μ§μ λλ€: μν νλλ₯Ό μ¬μ©ν΄ μ§μ μ 곡νκ±°λ λ·° νλ¬κ·ΈμΈμ μ¬μ©ν΄ κ°μ μ μΌλ‘ μ 곡νλ λ°©λ²μ λλ€.
λ·° νλ¬κ·ΈμΈκ³Ό μν νλ μ€ μ΄λ€ κ²μ μ¬μ©ν΄μΌ νλμ?
λ·° νλ¬κ·ΈμΈκ³Ό μν νλ λͺ¨λ λ°μ½λ μ΄μ μ μ 곡ν μ μμ§λ§ λͺ κ°μ§ μ°¨μ΄μ μ΄ μμ΅λλ€.
- Viewport λ΄λΆμ μλ λ΄μ©μ κΈ°λ°μΌλ‘ λ°μ½λ μ΄μ μ κ²°μ ν μ μλ€λ©΄ λ·° νλ¬κ·ΈμΈμ μ¬μ©νμΈμ.
- λ·°ν¬νΈ μΈλΆμ λ°μ½λ μ΄μ μ κ΄λ¦¬ν΄μΌ νλ€λ©΄ μν νλλ₯Ό μ¬μ©νμΈμ.
- μ€ λ°κΏ μΆκ°μ κ°μ΄ λ·°ν¬νΈ λ΄μ©μ λ³κ²½ν μ μλ λ³κ²½μ μνλ€λ©΄ μν νλλ₯Ό μ¬μ©νμΈμ.
λ μ κ·Ό λ°©μ μ€ μ΄λ κ²μΌλ‘λ νμ₯ κΈ°λ₯μ ꡬνν μ μλ€λ©΄ μΌλ°μ μΌλ‘ λ·° νλ¬κ·ΈμΈμ΄ λ λμ μ±λ₯μ μ 곡ν©λλ€. μλ₯Ό λ€μ΄ λ¬Έμμ λ§μΆ€λ²μ κ²μ¬νλ μλν° νμ₯ κΈ°λ₯μ ꡬννλ€κ³ κ°μ ν΄ λ³΄κ² μ΅λλ€.
ν κ°μ§ λ°©λ²μ μ 체 λ¬Έμλ₯Ό μΈλΆ λ§μΆ€λ² κ²μ¬κΈ°μ μ λ¬ν λ€μ λ§μΆ€λ² μ€λ₯ λͺ©λ‘μ λ°νλ°λ κ²μ λλ€. μ΄ κ²½μ° κ° μ€λ₯λ₯Ό λ°μ½λ μ΄μ μ λ§€ννκ³ νμ¬ λ·°ν¬νΈμ 무μμ΄ μλμ§μ κ΄κ³μμ΄ λ°μ½λ μ΄μ μ κ΄λ¦¬νκΈ° μν΄ μν νλλ₯Ό μ¬μ©ν΄μΌ ν©λλ€.
λ€λ₯Έ λ°©λ²μ λ·°ν¬νΈμ 보μ΄λ λ΄μ©λ§ λ§μΆ€λ² κ²μ¬λ₯Ό νλ κ²μ λλ€. νμ₯ κΈ°λ₯μ μ¬μ©μκ° λ¬Έμλ₯Ό μ€ν¬λ‘€ν λ μ§μμ μΌλ‘ λ§μΆ€λ² κ²μ¬λ₯Ό μ€νν΄μΌ νμ§λ§ μλ°±λ§ μ€μ ν μ€νΈκ° μλ λ¬Έμλ λ§μΆ€λ² κ²μ¬λ₯Ό ν μ μμ΅λλ€.
λ°μ½λ μ΄μ μ 곡
λΆλ¦Ώ 리μ€νΈ νλͺ©μ μ΄λͺ¨μ§λ‘ λ°κΎΈλ μλν° νμ₯ κΈ°λ₯μ λ§λ€κ³ μΆλ€κ³ κ°μ ν΄ λ³΄κ² μ΅λλ€. λ·° νλ¬κ·ΈμΈμ΄λ μν νλλ₯Ό μ¬μ©νμ¬ μ΄λ₯Ό ꡬνν μ μμΌλ©° κ°κ° μ½κ°μ μ°¨μ΄κ° μμ΅λλ€. μ΄ μΉμ μμλ λ μ νμ νμ₯ κΈ°λ₯μΌλ‘ ꡬννλ λ°©λ²μ μ΄ν΄λ³΄κ² μ΅λλ€.
λ ꡬν λͺ¨λ λμΌν ν΅μ¬ λ‘μ§μ 곡μ ν©λλ€:
- syntaxTreeλ₯Ό μ¬μ©νμ¬ λ¦¬μ€νΈ νλͺ© μ°ΎκΈ°
- κ° λ¦¬μ€νΈ νλͺ©μ λν΄ μ ν νμ΄ν
-
μ _μμ ―_μΌλ‘ κ΅μ²΄
μμ ―
μμ ―μ μλν°μ μΆκ°ν μ μλ 컀μ€ν HTML μμμ λλ€. λ¬Έμμ νΉμ μμΉμ μμ ―μ μ½μ νκ±°λ μ½ν μΈ μΌλΆλ₯Ό μμ ―μΌλ‘ κ΅μ²΄ν μ μμ΅λλ€.
λ€μ μμ λ HTML μμ <span>π</span>
μ λ°ννλ μμ ―μ μ μν©λλ€. λμ€μ μ΄ μμ ―μ μ¬μ©νκ² λ©λλ€.
import { EditorView, WidgetType } from '@codemirror/view';
export class EmojiWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
const div = document.createElement('span');
div.innerText = 'π';
return div;
}
}
λ¬Έμμ μ½ν μΈ λ²μλ₯Ό μ΄λͺ¨μ§ μμ ―μΌλ‘ κ΅μ²΄νλ €λ©΄ κ΅μ²΄ λ°μ½λ μ΄μ μ μ¬μ©νμΈμ.
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
μν νλ
μν νλμμ λ°μ½λ μ΄μ μ μ 곡νλ €λ©΄:
-
DecorationSet
νμ μΌλ‘ μν νλ μ μ -
μν νλμ
provide
μμ± μΆκ°provide(field: StateField<DecorationSet>): Extension { return EditorView.decorations.from(field); },
import { syntaxTree } from '@codemirror/language';
import {
Extension,
RangeSetBuilder,
StateField,
Transaction,
} from '@codemirror/state';
import {
Decoration,
DecorationSet,
EditorView,
WidgetType,
} from '@codemirror/view';
import { EmojiWidget } from 'emoji';
export const emojiListField = StateField.define<DecorationSet>({
create(state): DecorationSet {
return Decoration.none;
},
update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
syntaxTree(transaction.state).iterate({
enter(node) {
if (node.type.name.startsWith('list')) {
// '-' λλ '*'μ μμΉ
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
return builder.finish();
},
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
});
λ·° νλ¬κ·ΈμΈ
λ·° νλ¬κ·ΈμΈμ μ¬μ©νμ¬ λ°μ½λ μ΄μ μ κ΄λ¦¬νλ €λ©΄:
- λ·° νλ¬κ·ΈμΈ μμ±
- νλ¬κ·ΈμΈμ
DecorationSet
λ©€λ² μμ± μΆκ° constructor()
μμ λ°μ½λ μ΄μ μ΄κΈ°νupdate()
μμ λ°μ½λ μ΄μ μ¬κ΅¬μ±
λͺ¨λ μ λ°μ΄νΈκ° λ°μ½λ μ΄μ μ¬κ΅¬μ±μ μ΄μ λ μλλλ€. λ€μ μμ λ κΈ°λ³Έ λ¬Έμλ λ·°ν¬νΈκ° λ³κ²½λ λλ§ λ°μ½λ μ΄μ μ μ¬κ΅¬μ±ν©λλ€.
import { syntaxTree } from '@codemirror/language';
import { RangeSetBuilder } from '@codemirror/state';
import {
Decoration,
DecorationSet,
EditorView,
PluginSpec,
PluginValue,
ViewPlugin,
ViewUpdate,
WidgetType,
} from '@codemirror/view';
import { EmojiWidget } from 'emoji';
class EmojiListPlugin implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith('list')) {
// '-' λλ '*'μ μμΉ
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
}
return builder.finish();
}
}
const pluginSpec: PluginSpec<EmojiListPlugin> = {
decorations: (value: EmojiListPlugin) => value.decorations,
};
export const emojiListPlugin = ViewPlugin.fromClass(
EmojiListPlugin,
pluginSpec
);
buildDecorations()
λ μλν° λ·°λ₯Ό κΈ°λ°μΌλ‘ μμ ν λ°μ½λ μ΄μ
μΈνΈλ₯Ό ꡬμ±νλ λμ°λ―Έ λ©μλμ
λλ€.
ViewPlugin.fromClass()
ν¨μμ λ λ²μ§Έ μΈμμ μ£Όλͺ©νμΈμ. PluginSpec
μ decorations
μμ±μ λ·° νλ¬κ·ΈμΈμ΄ λ°μ½λ μ΄μ
μ μλν°μ μ 곡νλ λ°©λ²μ μ§μ ν©λλ€.
λ·° νλ¬κ·ΈμΈμ μ¬μ©μμκ² λ³΄μ΄λ λ΄μ©μ μκ³ μμΌλ―λ‘ view.visibleRanges
λ₯Ό μ¬μ©νμ¬ κ΅¬λ¬Έ νΈλ¦¬μμ λ°©λ¬Έν λΆλΆμ μ νν μ μμ΅λλ€.