λ°μ½”λ ˆμ΄μ…˜(Decorations)을 μ‚¬μš©ν•˜λ©΄ 에디터 ν™•μž₯ κΈ°λŠ₯μ—μ„œ μ½˜ν…μΈ λ₯Ό μ–΄λ–»κ²Œ κ·Έλ¦¬κ±°λ‚˜ μŠ€νƒ€μΌλ§ν• μ§€ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ—λ””ν„°μ—μ„œ μš”μ†Œλ₯Ό μΆ”κ°€, κ΅μ²΄ν•˜κ±°λ‚˜ μŠ€νƒ€μΌμ„ λ³€κ²½ν•˜μ—¬ λͺ¨μ–‘κ³Ό λŠλ‚Œμ„ λ°”κΎΈλ €λ©΄ λŒ€λΆ€λΆ„ λ°μ½”λ ˆμ΄μ…˜μ„ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이 νŽ˜μ΄μ§€λ₯Ό 읽고 λ‚˜λ©΄ λ‹€μŒμ„ ν•  수 있게 λ©λ‹ˆλ‹€:

  • 에디터 외관을 λ³€κ²½ν•˜κΈ° μœ„ν•΄ λ°μ½”λ ˆμ΄μ…˜μ„ μ‚¬μš©ν•˜λŠ” 방법 이해
  • μƒνƒœ ν•„λ“œμ™€ λ·° ν”ŒλŸ¬κ·ΈμΈμ„ μ‚¬μš©ν•˜μ—¬ λ°μ½”λ ˆμ΄μ…˜μ„ μ œκ³΅ν•˜λŠ” 차이점 이해

Note

이 νŽ˜μ΄μ§€λŠ” Obsidian ν”ŒλŸ¬κ·ΈμΈ 개발자λ₯Ό μœ„ν•΄ 곡식 CodeMirror 6 λ¬Έμ„œλ₯Ό μš”μ•½ν•œ κ²ƒμž…λ‹ˆλ‹€. μƒνƒœ ν•„λ“œμ— λŒ€ν•œ 더 μžμ„Έν•œ μ •λ³΄λŠ” Decorating the Documentλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

ν•„μˆ˜ 쑰건

κ°œμš”

λ°μ½”λ ˆμ΄μ…˜ μ—†μ΄λŠ” λ¬Έμ„œκ°€ 일반 ν…μŠ€νŠΈλ‘œ λ Œλ”λ§λ©λ‹ˆλ‹€. μ „ν˜€ ν₯λ―Έλ‘­μ§€ μ•Šμ£ . λ°μ½”λ ˆμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ ν…μŠ€νŠΈ κ°•μ‘°λ‚˜ μ»€μŠ€ν…€ HTML μš”μ†Œ 좔가와 같이 λ¬Έμ„œ ν‘œμ‹œ 방식을 λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒ μœ ν˜•μ˜ λ°μ½”λ ˆμ΄μ…˜μ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

λ°μ½”λ ˆμ΄μ…˜μ„ μ‚¬μš©ν•˜λ €λ©΄ 에디터 ν™•μž₯ κΈ°λŠ₯ λ‚΄λΆ€μ—μ„œ λ°μ½”λ ˆμ΄μ…˜μ„ μƒμ„±ν•˜κ³  ν™•μž₯ κΈ°λŠ₯이 이λ₯Ό 에디터에 _제곡_ν•˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€. λ°μ½”λ ˆμ΄μ…˜μ„ 에디터에 μ œκ³΅ν•˜λŠ” 방법은 두 κ°€μ§€μž…λ‹ˆλ‹€: μƒνƒœ ν•„λ“œλ₯Ό μ‚¬μš©ν•΄ 직접 μ œκ³΅ν•˜κ±°λ‚˜ λ·° ν”ŒλŸ¬κ·ΈμΈμ„ μ‚¬μš©ν•΄ κ°„μ ‘μ μœΌλ‘œ μ œκ³΅ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

λ·° ν”ŒλŸ¬κ·ΈμΈκ³Ό μƒνƒœ ν•„λ“œ 쀑 μ–΄λ–€ 것을 μ‚¬μš©ν•΄μ•Ό ν•˜λ‚˜μš”?

λ·° ν”ŒλŸ¬κ·ΈμΈκ³Ό μƒνƒœ ν•„λ“œ λͺ¨λ‘ λ°μ½”λ ˆμ΄μ…˜μ„ μ œκ³΅ν•  수 μžˆμ§€λ§Œ λͺ‡ κ°€μ§€ 차이점이 μžˆμŠ΅λ‹ˆλ‹€.

  • Viewport 내뢀에 μžˆλŠ” λ‚΄μš©μ„ 기반으둜 λ°μ½”λ ˆμ΄μ…˜μ„ κ²°μ •ν•  수 μžˆλ‹€λ©΄ λ·° ν”ŒλŸ¬κ·ΈμΈμ„ μ‚¬μš©ν•˜μ„Έμš”.
  • 뷰포트 μ™ΈλΆ€μ˜ λ°μ½”λ ˆμ΄μ…˜μ„ 관리해야 ν•œλ‹€λ©΄ μƒνƒœ ν•„λ“œλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
  • 쀄 λ°”κΏˆ 좔가와 같이 뷰포트 λ‚΄μš©μ„ λ³€κ²½ν•  수 μžˆλŠ” 변경을 μ›ν•œλ‹€λ©΄ μƒνƒœ ν•„λ“œλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.

두 μ ‘κ·Ό 방식 쀑 μ–΄λŠ κ²ƒμœΌλ‘œλ„ ν™•μž₯ κΈ°λŠ₯을 κ΅¬ν˜„ν•  수 μžˆλ‹€λ©΄ 일반적으둜 λ·° ν”ŒλŸ¬κ·ΈμΈμ΄ 더 λ‚˜μ€ μ„±λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ λ¬Έμ„œμ˜ λ§žμΆ€λ²•μ„ κ²€μ‚¬ν•˜λŠ” 에디터 ν™•μž₯ κΈ°λŠ₯을 κ΅¬ν˜„ν•œλ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

ν•œ κ°€μ§€ 방법은 전체 λ¬Έμ„œλ₯Ό μ™ΈλΆ€ λ§žμΆ€λ²• 검사기에 μ „λ‹¬ν•œ λ‹€μŒ λ§žμΆ€λ²• 였λ₯˜ λͺ©λ‘μ„ λ°˜ν™˜λ°›λŠ” κ²ƒμž…λ‹ˆλ‹€. 이 경우 각 였λ₯˜λ₯Ό λ°μ½”λ ˆμ΄μ…˜μ— λ§€ν•‘ν•˜κ³  ν˜„μž¬ λ·°ν¬νŠΈμ— 무엇이 μžˆλŠ”μ§€μ™€ 관계없이 λ°μ½”λ ˆμ΄μ…˜μ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ μƒνƒœ ν•„λ“œλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

λ‹€λ₯Έ 방법은 λ·°ν¬νŠΈμ— λ³΄μ΄λŠ” λ‚΄μš©λ§Œ λ§žμΆ€λ²• 검사λ₯Ό ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. ν™•μž₯ κΈ°λŠ₯은 μ‚¬μš©μžκ°€ λ¬Έμ„œλ₯Ό μŠ€ν¬λ‘€ν•  λ•Œ μ§€μ†μ μœΌλ‘œ λ§žμΆ€λ²• 검사λ₯Ό μ‹€ν–‰ν•΄μ•Ό ν•˜μ§€λ§Œ 수백만 μ€„μ˜ ν…μŠ€νŠΈκ°€ μžˆλŠ” λ¬Έμ„œλ„ λ§žμΆ€λ²• 검사λ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μƒνƒœ ν•„λ“œ vs λ·° ν”ŒλŸ¬κ·ΈμΈ

λ°μ½”λ ˆμ΄μ…˜ 제곡

뢈릿 리슀트 ν•­λͺ©μ„ 이λͺ¨μ§€λ‘œ λ°”κΎΈλŠ” 에디터 ν™•μž₯ κΈ°λŠ₯을 λ§Œλ“€κ³  μ‹Άλ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€. λ·° ν”ŒλŸ¬κ·ΈμΈμ΄λ‚˜ μƒνƒœ ν•„λ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ 이λ₯Ό κ΅¬ν˜„ν•  수 있으며 각각 μ•½κ°„μ˜ 차이가 μžˆμŠ΅λ‹ˆλ‹€. 이 μ„Ήμ…˜μ—μ„œλŠ” 두 μœ ν˜•μ˜ ν™•μž₯ κΈ°λŠ₯으둜 κ΅¬ν˜„ν•˜λŠ” 방법을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

두 κ΅¬ν˜„ λͺ¨λ‘ λ™μΌν•œ 핡심 λ‘œμ§μ„ κ³΅μœ ν•©λ‹ˆλ‹€:

  1. syntaxTreeλ₯Ό μ‚¬μš©ν•˜μ—¬ 리슀트 ν•­λͺ© μ°ΎκΈ°
  2. 각 리슀트 ν•­λͺ©μ— λŒ€ν•΄ μ„ ν–‰ ν•˜μ΄ν”ˆ -을 _μœ„μ ―_으둜 ꡐ체

μœ„μ ―

μœ„μ ―μ€ 에디터에 μΆ”κ°€ν•  수 μžˆλŠ” μ»€μŠ€ν…€ 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()
});

μƒνƒœ ν•„λ“œ

μƒνƒœ ν•„λ“œμ—μ„œ λ°μ½”λ ˆμ΄μ…˜μ„ μ œκ³΅ν•˜λ €λ©΄:

  1. DecorationSet νƒ€μž…μœΌλ‘œ μƒνƒœ ν•„λ“œ μ •μ˜

  2. μƒνƒœ ν•„λ“œμ— 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);
  },
});

λ·° ν”ŒλŸ¬κ·ΈμΈ

λ·° ν”ŒλŸ¬κ·ΈμΈμ„ μ‚¬μš©ν•˜μ—¬ λ°μ½”λ ˆμ΄μ…˜μ„ κ΄€λ¦¬ν•˜λ €λ©΄:

  1. λ·° ν”ŒλŸ¬κ·ΈμΈ 생성
  2. ν”ŒλŸ¬κ·ΈμΈμ— DecorationSet 멀버 속성 μΆ”κ°€
  3. constructor()μ—μ„œ λ°μ½”λ ˆμ΄μ…˜ μ΄ˆκΈ°ν™”
  4. 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λ₯Ό μ‚¬μš©ν•˜μ—¬ ꡬ문 νŠΈλ¦¬μ—μ„œ λ°©λ¬Έν•  뢀뢄을 μ œν•œν•  수 μžˆμŠ΅λ‹ˆλ‹€.