mdsquare 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import type { MarkdownConfig } from "@lezer/markdown";
2
+ /** 정의 내용으로 이어지는 줄의 들여쓰기 (GitHub/Pandoc 방식) */
3
+ export declare const FOOTNOTE_INDENT = 4;
2
4
  /**
3
5
  * 각주 문법입니다.
4
6
  *
@@ -9,15 +11,16 @@ import type { MarkdownConfig } from "@lezer/markdown";
9
11
  * FootnoteLabel "label"
10
12
  * FootnoteMark "]"
11
13
  * ```
12
- * 정의 `[^label]: 내용` (최상위 문단, 내용은 인라인 문법으로 파싱):
14
+ * 정의 `[^label]: 내용` (최상위 블록, 목록 항목처럼 블록을 담는 컨테이너):
13
15
  * ```
14
16
  * FootnoteDefinition
15
17
  * FootnoteMark "[^"
16
18
  * FootnoteLabel "label"
17
19
  * FootnoteMark "]:"
18
- * ...내용
20
+ * Paragraph / Table / FencedCode / BlockMath / BulletList ...
19
21
  * ```
20
22
  * - 라벨: 공백, `[`, `]`, `^`를 제외한 문자 (`1`, `note-1`, `각주` 등)
21
- * - 정의는 하나의 문단입니다. (빈 줄 뒤 들여쓴 여러 문단 각주는 지원하지 않습니다)
23
+ * - 4칸 들여쓴 줄(빈 줄 포함)은 정의 내용입니다. 여러 문단, 표, 수식, 코드 블록 등을 넣을 수 있습니다.
24
+ * 들여쓰지 않은 줄은 문단을 이어 쓰는 경우(게으른 이어쓰기)만 정의에 붙습니다.
22
25
  */
23
26
  export declare const Footnote: MarkdownConfig;
@@ -9,33 +9,10 @@ function isLabelBreak(code) {
9
9
  return true;
10
10
  return code >= 128 && /\s/.test(String.fromCharCode(code));
11
11
  }
12
- /** 정의: 문단이 `[^label]:`로 시작 */
13
- const DEFINITION_RE = /^\[\^([^\s[\]^]+)\]:/;
14
- class FootnoteDefinitionParser {
15
- nextLine(_cx, _line, leaf) {
16
- // 각주 정의 문단은 이 파서가 맡습니다. 뒤에 등록된 파서(링크 참조 정의 등)가
17
- // 다음 줄에서 블록을 먼저 끝내지 않도록 제거합니다. (이 파서가 먼저 호출되므로 제거된 파서는 호출되지 않음)
18
- if (leaf.parsers.length > 1)
19
- leaf.parsers.splice(0, leaf.parsers.length, this);
20
- // 이어지는 줄(빈 줄 전까지)은 모두 정의 내용입니다.
21
- return false;
22
- }
23
- finish(cx, leaf) {
24
- const match = DEFINITION_RE.exec(leaf.content);
25
- if (!match)
26
- return false;
27
- const start = leaf.start;
28
- const labelTo = start + 2 + match[1].length;
29
- const markTo = labelTo + 2; // "]:"
30
- cx.addLeafElement(leaf, cx.elt("FootnoteDefinition", start, start + leaf.content.length, [
31
- cx.elt("FootnoteMark", start, start + 2),
32
- cx.elt("FootnoteLabel", start + 2, labelTo),
33
- cx.elt("FootnoteMark", labelTo, markTo),
34
- ...cx.parser.parseInline(leaf.content.slice(markTo - start), markTo),
35
- ]));
36
- return true;
37
- }
38
- }
12
+ /** 정의 시작: `[^label]:` (뒤 공백 하나까지) */
13
+ const DEFINITION_RE = /^\[\^([^\s[\]^]+)\]:[ \t]?/;
14
+ /** 정의 내용으로 이어지는 줄의 들여쓰기 (GitHub/Pandoc 방식) */
15
+ export const FOOTNOTE_INDENT = 4;
39
16
  /**
40
17
  * 각주 문법입니다.
41
18
  *
@@ -46,21 +23,32 @@ class FootnoteDefinitionParser {
46
23
  * FootnoteLabel "label"
47
24
  * FootnoteMark "]"
48
25
  * ```
49
- * 정의 `[^label]: 내용` (최상위 문단, 내용은 인라인 문법으로 파싱):
26
+ * 정의 `[^label]: 내용` (최상위 블록, 목록 항목처럼 블록을 담는 컨테이너):
50
27
  * ```
51
28
  * FootnoteDefinition
52
29
  * FootnoteMark "[^"
53
30
  * FootnoteLabel "label"
54
31
  * FootnoteMark "]:"
55
- * ...내용
32
+ * Paragraph / Table / FencedCode / BlockMath / BulletList ...
56
33
  * ```
57
34
  * - 라벨: 공백, `[`, `]`, `^`를 제외한 문자 (`1`, `note-1`, `각주` 등)
58
- * - 정의는 하나의 문단입니다. (빈 줄 뒤 들여쓴 여러 문단 각주는 지원하지 않습니다)
35
+ * - 4칸 들여쓴 줄(빈 줄 포함)은 정의 내용입니다. 여러 문단, 표, 수식, 코드 블록 등을 넣을 수 있습니다.
36
+ * 들여쓰지 않은 줄은 문단을 이어 쓰는 경우(게으른 이어쓰기)만 정의에 붙습니다.
59
37
  */
60
38
  export const Footnote = {
61
39
  defineNodes: [
62
40
  { name: "FootnoteReference" },
63
- { name: "FootnoteDefinition", block: true },
41
+ {
42
+ name: "FootnoteDefinition",
43
+ block: true,
44
+ // 이어지는 줄: 4칸 이상 들여썼거나 빈 줄이면 정의 안 (들여쓰기를 건너뜀)
45
+ composite(_cx, line, value) {
46
+ if (line.indent < line.baseIndent + value && line.next > -1)
47
+ return false;
48
+ line.moveBaseColumn(line.baseIndent + value);
49
+ return true;
50
+ },
51
+ },
64
52
  { name: "FootnoteMark" },
65
53
  { name: "FootnoteLabel" },
66
54
  ],
@@ -89,16 +77,26 @@ export const Footnote = {
89
77
  parseBlock: [
90
78
  {
91
79
  name: "FootnoteDefinition",
92
- leaf(cx, leaf) {
93
- // 정의는 최상위 문단에서만 인식합니다.
94
- return cx.parentType().name === "Document" && DEFINITION_RE.test(leaf.content)
95
- ? new FootnoteDefinitionParser()
96
- : null;
80
+ parse(cx, line) {
81
+ // 정의는 최상위에서만 시작합니다.
82
+ if (cx.parentType().name !== "Document")
83
+ return false;
84
+ const match = DEFINITION_RE.exec(line.text.slice(line.pos));
85
+ if (!match)
86
+ return false;
87
+ const start = cx.lineStart + line.pos;
88
+ const labelTo = start + 2 + match[1].length;
89
+ cx.startComposite("FootnoteDefinition", line.pos, FOOTNOTE_INDENT);
90
+ cx.addElement(cx.elt("FootnoteMark", start, start + 2));
91
+ cx.addElement(cx.elt("FootnoteLabel", start + 2, labelTo));
92
+ cx.addElement(cx.elt("FootnoteMark", labelTo, labelTo + 2));
93
+ line.moveBase(line.pos + match[0].length);
94
+ return null;
97
95
  },
98
96
  // 빈 줄 없이 연속한 정의(`[^1]: …` 다음 줄에 `[^2]: …`)를 따로 인식하도록,
99
- // `[^label]:`로 시작하는 줄이 앞 문단(또는 앞 정의)을 끝내게 합니다.
100
- endLeaf(cx, line) {
101
- return cx.parentType().name === "Document" && DEFINITION_RE.test(line.text.slice(line.pos));
97
+ // 들여쓰지 않은 `[^label]:` 줄이 앞 문단(앞 정의의 문단 포함)을 끝내게 합니다.
98
+ endLeaf(_cx, line) {
99
+ return line.indent < FOOTNOTE_INDENT && DEFINITION_RE.test(line.text.slice(line.pos)) && line.basePos === 0;
102
100
  },
103
101
  // 링크 참조 정의(`[label]: url`)보다 먼저 처리합니다.
104
102
  before: "LinkReference",
package/dist/hashtag.js CHANGED
@@ -45,9 +45,14 @@ export const hashtagPlugin = createRevealPlugin({
45
45
  const hashtagTheme = EditorView.baseTheme({
46
46
  ".cm-hashtag": {
47
47
  backgroundColor: "#008CFF",
48
- // `.cm-hashtag *`로 지정하면 안쪽 span이 있을 때만 적용되어
49
- // (구문 강조 span이 없으면 적용되지 않음) 글자 크기가 들쭉날쭉해지므로 직접 지정합니다.
50
- fontSize: "0.75rem",
48
+ // 주변 글자의 0.9배 (제목 안에서는 제목 크기를 따라 커짐).
49
+ // `.cm-hashtag *`가 아니라 여기에 지정해야 안쪽 span 유무와 상관없이 같은 크기가 됩니다.
50
+ fontSize: "0.9em",
51
+ },
52
+ // 파란 배경 위 흰 글자. 안쪽 span(구문 강조, 계층 구분자 '/')까지 흰색으로 하고,
53
+ // 커서가 닿아 기호(cm-formatting)가 되어도 제목 안 기호 규칙(제목 색)보다 우선하도록 선택자를 구체적으로 씁니다.
54
+ ".cm-content .cm-line .cm-hashtag.cm-hashtag-begin, .cm-content .cm-line .cm-hashtag.cm-hashtag-end, .cm-content .cm-line .cm-hashtag.cm-hashtag-begin *, .cm-content .cm-line .cm-hashtag.cm-hashtag-end *": {
55
+ color: "#fff",
51
56
  },
52
57
  ".cm-hashtag.cm-hashtag-begin": {
53
58
  borderRadius: "2px 0 0 2px",
package/dist/heading.js CHANGED
@@ -14,6 +14,7 @@ const CONTAINERS = new Set([
14
14
  "BulletList",
15
15
  "OrderedList",
16
16
  "ListItem",
17
+ "FootnoteDefinition",
17
18
  ]);
18
19
  // 데코레이션 객체는 모듈 로드 시 한 번만 만들고 재사용합니다. (index = level)
19
20
  const levels = [0, 1, 2, 3, 4, 5, 6];
@@ -116,6 +117,11 @@ const headingTheme = EditorView.baseTheme({
116
117
  ".cm-line .cm-formatting-heading *": {
117
118
  color: "inherit",
118
119
  },
120
+ // 제목 안의 다른 서식 기호(==, **, *, ~~, --, `, [ ]( ) 등)도 커서가 닿아 보일 때 제목 색을 따릅니다.
121
+ // (구문 강조가 기호의 안쪽 span에 회색을 직접 지정해 제목 색이 무시되던 문제)
122
+ ".cm-line.cm-heading .cm-formatting, .cm-line.cm-heading .cm-formatting *": {
123
+ color: "inherit",
124
+ },
119
125
  ".cm-heading-line *": {
120
126
  textDecoration: "none",
121
127
  },
@@ -42,6 +42,7 @@ const CONTAINERS = new Set([
42
42
  "BulletList",
43
43
  "OrderedList",
44
44
  "ListItem",
45
+ "FootnoteDefinition",
45
46
  ]);
46
47
  /**
47
48
  * 구분선은 블록이므로, 문서 전체 트리를 훑지 않고 블록 컨테이너만 내려가며 찾습니다.
package/dist/link.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type EditorState, type Extension } from "@codemirror/state";
2
+ import { ViewPlugin } from "@codemirror/view";
2
3
  import type { SyntaxNode } from "@lezer/common";
3
4
  export interface InlineLink {
4
5
  /** 링크 전체 범위 */
@@ -25,7 +26,33 @@ export interface InlineLink {
25
26
  * 참조 링크(`[a][ref]`, `[a]`)처럼 인라인 링크가 아니면 null을 반환합니다.
26
27
  */
27
28
  export declare function readInlineLink(state: EditorState, link: SyntaxNode): InlineLink | null;
28
- export declare const linkPlugin: import("@codemirror/view").ViewPlugin<import("./extensions.js").RevealPluginValue, undefined>;
29
+ /** 기본 아이콘 주소: 사이트의 /favicon.ico (다른 서비스에 주소를 보내지 않음) */
30
+ export declare function defaultFavicon(url: URL): string;
31
+ /**
32
+ * 글자 폭(넓은 글자는 2칸)이 max를 넘으면 잘라서 끝에 "…"를 붙입니다.
33
+ * 공백은 하나로 줄이고 앞뒤 공백은 없앱니다.
34
+ */
35
+ export declare function truncateTitle(text: string, max: number): string;
36
+ /** HTML에서 페이지 제목을 읽습니다. (og:title → twitter:title → <title>) */
37
+ export declare function parseHtmlTitle(html: string): string | null;
38
+ /**
39
+ * HTML 바이트를 문자 집합에 맞게 글자로 바꿉니다. (response.text()는 항상 UTF-8로 읽어 EUC-KR 등이 깨짐)
40
+ * Content-Type의 charset → 문서 앞부분의 <meta charset> → UTF-8 순서로 정합니다.
41
+ */
42
+ export declare function decodeHtml(bytes: ArrayBuffer | Uint8Array, contentType: string | null): string;
43
+ export interface FetchPageTitleOptions {
44
+ /**
45
+ * 실제로 요청할 주소. 브라우저는 CORS 때문에 다른 사이트의 HTML을 직접 읽을 수 없으므로
46
+ * 보통 서버의 중계 주소를 돌려줍니다. 예: `(url) => "/api/fetch?url=" + encodeURIComponent(url)`
47
+ * (Electron/Tauri처럼 CORS 제한이 없으면 생략)
48
+ */
49
+ proxy?: (url: string) => string;
50
+ /** 기다리는 최대 시간(ms). 기본 8000 */
51
+ timeout?: number;
52
+ }
53
+ /** 페이지 HTML을 받아 제목을 읽습니다. 실패하면 null. (`link({ fetchTitle })`에 넘기기 위한 도우미) */
54
+ export declare function fetchPageTitle(url: string, options?: FetchPageTitleOptions): Promise<string | null>;
55
+ export declare const linkPlugin: ViewPlugin<import("./extensions.js").RevealPluginValue, undefined>;
29
56
  /** 기본으로 열어도 안전한 주소인지 (http(s), mailto, tel, ftp) */
30
57
  export declare function isSafeUrl(href: string): boolean;
31
58
  /** 기본 열기 동작: 안전한 절대 주소만 새 탭으로 엽니다. */
@@ -36,12 +63,31 @@ export interface LinkOptions {
36
63
  * 상대 경로(`./doc.md`)나 앱 내부 이동을 처리하려면 직접 지정하세요.
37
64
  */
38
65
  openLink?: (href: string, event: MouseEvent) => void;
66
+ /**
67
+ * http(s) 링크 앞에 사이트 아이콘(favicon)을 보여줄지. 기본값 true
68
+ * - true: 사이트의 `/favicon.ico` (불러오지 못하면 기본 링크 아이콘)
69
+ * - 함수: 주소 → 아이콘 주소 (null이면 기본 링크 아이콘). 예: 아이콘 서비스 사용
70
+ * `(url) => "https://www.google.com/s2/favicons?sz=32&domain=" + url.hostname`
71
+ * - false: 아이콘 없음
72
+ * 아이콘을 불러올 때 링크된 사이트(또는 아이콘 서비스)에 요청이 갑니다.
73
+ */
74
+ favicon?: boolean | ((url: URL) => string | null);
75
+ /**
76
+ * 라벨이 빈 링크(`[](https://...)`)에 보여줄 페이지 제목을 가져오는 함수. 기본값: 없음(주소를 보여줌)
77
+ * 원문은 바꾸지 않고 화면에만 보여줍니다. 브라우저에서는 CORS 때문에 다른 사이트의 HTML을
78
+ * 직접 읽을 수 없으므로 보통 서버 중계가 필요합니다. `fetchPageTitle(url, { proxy })`를 쓸 수 있습니다.
79
+ */
80
+ fetchTitle?: (url: string) => Promise<string | null | undefined>;
81
+ /** 가져온 제목의 최대 길이 (글자 폭 기준, 한글 등 넓은 글자는 2칸). 넘으면 잘라서 "…". 기본값 40 */
82
+ maxTitleLength?: number;
39
83
  }
40
84
  /**
41
85
  * 인라인 링크 `[label](url "title")`를 하이브리드 방식으로 보여주는 확장입니다.
42
86
  * - 커서가 링크에 닿지 않으면 기호와 URL을 숨기고 라벨만 링크 스타일로 보여줍니다.
43
87
  * (라벨 안의 굵게/코드 등 인라인 문법은 그대로 유지)
44
88
  * - 렌더링된 라벨을 클릭하면 링크를 엽니다. (기본: http(s)/mailto/tel/ftp만 새 탭으로)
89
+ * - http(s) 링크 앞에 사이트 아이콘을 보여줍니다. (`favicon`)
90
+ * - 라벨이 빈 링크(`[](url)`)는 `fetchTitle`이 있으면 페이지 제목(길면 잘라서)을, 없으면 주소를 보여줍니다.
45
91
  * - 참조 링크(`[a][ref]`, `[a]`), 자동 링크, 이미지는 다루지 않습니다.
46
92
  */
47
93
  export declare function link(options?: LinkOptions): Extension;