@incremark/vue 0.1.0 → 0.2.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,13 +1,15 @@
1
1
  <script setup lang="ts">
2
- import type { PhrasingContent, Text, HTML } from 'mdast'
2
+ import type { PhrasingContent, RootContent, ImageReference, LinkReference } from 'mdast'
3
3
  import type { TextChunk } from '@incremark/core'
4
+ import {
5
+ type TextNodeWithChunks,
6
+ hasChunks,
7
+ getStableText,
8
+ isHtmlNode
9
+ } from '@incremark/shared'
4
10
  import IncremarkMath from './IncremarkMath.vue'
5
-
6
- // 扩展的文本节点类型(支持 chunks)
7
- interface TextNodeWithChunks extends Text {
8
- stableLength?: number
9
- chunks?: TextChunk[]
10
- }
11
+ import IncremarkHtmlElement from './IncremarkHtmlElement.vue'
12
+ import { useDefinationsContext } from '../composables/useDefinationsContext'
11
13
 
12
14
  // Math 节点类型
13
15
  interface MathNode {
@@ -15,44 +17,54 @@ interface MathNode {
15
17
  value: string
16
18
  }
17
19
 
18
- defineProps<{
19
- nodes: PhrasingContent[]
20
- }>()
20
+ // HtmlElement 节点类型
21
+ interface HtmlElementNode {
22
+ type: 'htmlElement'
23
+ tagName: string
24
+ attrs: Record<string, string>
25
+ children: RootContent[]
26
+ }
21
27
 
22
28
  /**
23
- * 获取文本节点的稳定部分(不需要动画)
29
+ * 类型守卫:检查是否是 htmlElement 节点
24
30
  */
25
- function getStableText(node: TextNodeWithChunks): string {
26
- if (!node.chunks || node.chunks.length === 0) {
27
- return node.value
28
- }
29
- return node.value.slice(0, node.stableLength ?? 0)
31
+ function isHtmlElementNode(node: PhrasingContent): node is PhrasingContent & HtmlElementNode {
32
+ return (node as unknown as HtmlElementNode).type === 'htmlElement'
33
+ }
34
+
35
+ /**
36
+ * 类型守卫:检查是否是 imageReference 节点
37
+ */
38
+ function isImageReference(node: PhrasingContent): node is ImageReference {
39
+ return node.type === 'imageReference'
30
40
  }
31
41
 
32
42
  /**
33
- * 类型守卫:检查是否是带 chunks 的文本节点
43
+ * 类型守卫:检查是否是 linkReference 节点
34
44
  */
35
- function hasChunks(node: PhrasingContent): node is TextNodeWithChunks {
36
- return node.type === 'text' && 'chunks' in node && Array.isArray((node as TextNodeWithChunks).chunks)
45
+ function isLinkReference(node: PhrasingContent): node is LinkReference {
46
+ return node.type === 'linkReference'
37
47
  }
38
48
 
49
+ const props = defineProps<{
50
+ nodes: PhrasingContent[]
51
+ }>()
52
+
53
+ const {
54
+ definations,
55
+ footnoteDefinitions
56
+ } = useDefinationsContext()
57
+
39
58
  /**
40
59
  * 获取节点的 chunks(类型安全)
41
60
  */
42
61
  function getChunks(node: PhrasingContent): TextChunk[] | undefined {
43
62
  if (hasChunks(node)) {
44
- return node.chunks
63
+ return (node as TextNodeWithChunks).chunks
45
64
  }
46
65
  return undefined
47
66
  }
48
67
 
49
- /**
50
- * 类型守卫:检查是否是 HTML 节点
51
- */
52
- function isHtmlNode(node: PhrasingContent): node is HTML {
53
- return node.type === 'html'
54
- }
55
-
56
68
  /**
57
69
  * 类型守卫:检查是否是 inlineMath 节点
58
70
  * inlineMath 是 mdast-util-math 扩展的类型,不在标准 PhrasingContent 中
@@ -60,6 +72,7 @@ function isHtmlNode(node: PhrasingContent): node is HTML {
60
72
  function isInlineMath(node: PhrasingContent): node is PhrasingContent & MathNode {
61
73
  return (node as unknown as MathNode).type === 'inlineMath'
62
74
  }
75
+
63
76
  </script>
64
77
 
65
78
  <template>
@@ -79,6 +92,15 @@ function isInlineMath(node: PhrasingContent): node is PhrasingContent & MathNode
79
92
  <!-- 行内公式 -->
80
93
  <IncremarkMath v-else-if="isInlineMath(node)" :node="(node as unknown as MathNode)" />
81
94
 
95
+ <!-- htmlElement 节点(结构化的 HTML 元素) -->
96
+ <IncremarkHtmlElement
97
+ v-else-if="isHtmlElementNode(node)"
98
+ :node="(node as unknown as HtmlElementNode)"
99
+ />
100
+
101
+ <!-- HTML 节点(原始 HTML,如未启用 htmlTree 选项) -->
102
+ <span v-else-if="isHtmlNode(node)" style="display: contents;" v-html="(node as any).value"></span>
103
+
82
104
  <!-- 加粗 -->
83
105
  <strong v-else-if="node.type === 'strong'">
84
106
  <IncremarkInline :nodes="(node.children as PhrasingContent[])" />
@@ -90,14 +112,15 @@ function isInlineMath(node: PhrasingContent): node is PhrasingContent & MathNode
90
112
  </em>
91
113
 
92
114
  <!-- 行内代码 -->
93
- <code v-else-if="node.type === 'inlineCode'" class="incremark-inline-code">{{ node.value }}</code>
115
+ <code v-else-if="node.type === 'inlineCode'" class="incremark-inline-code">{{ (node as any).value }}</code>
94
116
 
95
117
  <!-- 链接 -->
96
118
  <a
97
119
  v-else-if="node.type === 'link'"
120
+ class="incremark-link"
98
121
  :href="node.url"
99
122
  target="_blank"
100
- rel="noopener"
123
+ rel="noopener noreferrer"
101
124
  >
102
125
  <IncremarkInline :nodes="(node.children as PhrasingContent[])" />
103
126
  </a>
@@ -105,11 +128,54 @@ function isInlineMath(node: PhrasingContent): node is PhrasingContent & MathNode
105
128
  <!-- 图片 -->
106
129
  <img
107
130
  v-else-if="node.type === 'image'"
131
+ class="incremark-image"
108
132
  :src="node.url"
109
133
  :alt="node.alt || ''"
134
+ :title="(node as any).title || undefined"
110
135
  loading="lazy"
111
136
  />
112
137
 
138
+ <!-- 引用式图片(imageReference) -->
139
+ <template v-else-if="isImageReference(node)">
140
+ <img
141
+ v-if="definations[node.identifier]"
142
+ class="incremark-image incremark-reference-image"
143
+ :src="definations[node.identifier].url"
144
+ :alt="(node as ImageReference).alt || ''"
145
+ :title="definations[node.identifier].title || undefined"
146
+ loading="lazy"
147
+ />
148
+ <!-- 如果没有找到定义,渲染为原始文本(降级处理) -->
149
+ <span v-else class="incremark-image-ref-missing">
150
+ ![{{ (node as ImageReference).alt }}][{{ (node as ImageReference).identifier || (node as ImageReference).label }}]
151
+ </span>
152
+ </template>
153
+
154
+ <!-- 引用式链接(linkReference) -->
155
+ <template v-else-if="isLinkReference(node)">
156
+ <a
157
+ v-if="definations[node.identifier]"
158
+ class="incremark-link incremark-reference-link"
159
+ :href="definations[node.identifier].url"
160
+ :title="definations[node.identifier].title || undefined"
161
+ target="_blank"
162
+ rel="noopener noreferrer"
163
+ >
164
+ <IncremarkInline :nodes="((node as LinkReference).children as PhrasingContent[])" />
165
+ </a>
166
+ <!-- 如果没有找到定义,渲染为原始文本(降级处理) -->
167
+ <span v-else class="incremark-link-ref-missing">
168
+ [{{ ((node as LinkReference).children as any[]).map((c: any) => c.value).join('') }}][{{ (node as LinkReference).identifier || (node as LinkReference).label }}]
169
+ </span>
170
+ </template>
171
+
172
+ <!-- 脚注引用(footnoteReference) -->
173
+ <sup v-else-if="node.type === 'footnoteReference'" class="incremark-footnote-ref">
174
+ <a :href="`#fn-${(node as any).identifier}`" :id="`fnref-${(node as any).identifier}`">
175
+ [{{ (node as any).identifier }}]
176
+ </a>
177
+ </sup>
178
+
113
179
  <!-- 换行 -->
114
180
  <br v-else-if="node.type === 'break'" />
115
181
 
@@ -117,32 +183,5 @@ function isInlineMath(node: PhrasingContent): node is PhrasingContent & MathNode
117
183
  <del v-else-if="node.type === 'delete'">
118
184
  <IncremarkInline :nodes="(node.children as PhrasingContent[])" />
119
185
  </del>
120
-
121
- <!-- 原始 HTML -->
122
- <span v-else-if="isHtmlNode(node)" v-html="node.value"></span>
123
186
  </template>
124
187
  </template>
125
-
126
- <style>
127
- .incremark-inline-code {
128
- padding: 0.2em 0.4em;
129
- background: rgba(0, 0, 0, 0.06);
130
- border-radius: 4px;
131
- font-family: 'Fira Code', 'SF Mono', Consolas, monospace;
132
- font-size: 0.9em;
133
- }
134
-
135
- /* 渐入动画 */
136
- .incremark-fade-in {
137
- animation: incremark-fade-in 0.4s ease-out;
138
- }
139
-
140
- @keyframes incremark-fade-in {
141
- from {
142
- opacity: 0;
143
- }
144
- to {
145
- opacity: 1;
146
- }
147
- }
148
- </style>
@@ -44,40 +44,3 @@ function getItemContent(item: ListItem): PhrasingContent[] {
44
44
  </component>
45
45
  </template>
46
46
 
47
- <style scoped>
48
- .incremark-list {
49
- margin: 0.75em 0;
50
- padding-left: 2em;
51
- }
52
-
53
- .incremark-list.task-list {
54
- list-style: none;
55
- padding-left: 0;
56
- }
57
-
58
- .incremark-list-item {
59
- margin: 0.25em 0;
60
- line-height: 1.6;
61
- }
62
-
63
- .task-item {
64
- list-style: none;
65
- }
66
-
67
- .task-label {
68
- display: flex;
69
- align-items: flex-start;
70
- gap: 0.5em;
71
- cursor: default;
72
- }
73
-
74
- .checkbox {
75
- margin-top: 0.3em;
76
- flex-shrink: 0;
77
- }
78
-
79
- .task-content {
80
- flex: 1;
81
- }
82
- </style>
83
-
@@ -103,48 +103,3 @@ watch(formula, scheduleRender, { immediate: true })
103
103
  <pre v-else class="math-source-block"><code>{{ formula }}</code></pre>
104
104
  </div>
105
105
  </template>
106
-
107
- <style scoped>
108
- .incremark-math-inline {
109
- display: inline;
110
- }
111
-
112
- .incremark-math-block {
113
- margin: 1em 0;
114
- padding: 1em;
115
- overflow-x: auto;
116
- text-align: center;
117
- }
118
-
119
- .math-source {
120
- background: #f3f4f6;
121
- padding: 0.1em 0.3em;
122
- border-radius: 3px;
123
- font-size: 0.9em;
124
- color: #6b7280;
125
- }
126
-
127
- .math-source-block {
128
- margin: 0;
129
- padding: 1em;
130
- background: #f3f4f6;
131
- border-radius: 6px;
132
- text-align: left;
133
- }
134
-
135
- .math-source-block code {
136
- font-family: 'Fira Code', monospace;
137
- font-size: 0.9em;
138
- color: #374151;
139
- }
140
-
141
- .math-rendered :deep(.katex) {
142
- font-size: 1.1em;
143
- }
144
-
145
- .math-rendered :deep(.katex-display) {
146
- margin: 0;
147
- overflow-x: auto;
148
- overflow-y: hidden;
149
- }
150
- </style>
@@ -12,10 +12,3 @@ defineProps<{
12
12
  <IncremarkInline :nodes="node.children" />
13
13
  </p>
14
14
  </template>
15
-
16
- <style scoped>
17
- .incremark-paragraph {
18
- margin: 0.75em 0;
19
- line-height: 1.6;
20
- }
21
- </style>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { RootContent } from 'mdast'
2
+ import type { RootContent, HTML } from 'mdast'
3
3
  import type { Component } from 'vue'
4
4
  import IncremarkHeading from './IncremarkHeading.vue'
5
5
  import IncremarkParagraph from './IncremarkParagraph.vue'
@@ -9,9 +9,10 @@ import IncremarkTable from './IncremarkTable.vue'
9
9
  import IncremarkBlockquote from './IncremarkBlockquote.vue'
10
10
  import IncremarkThematicBreak from './IncremarkThematicBreak.vue'
11
11
  import IncremarkMath from './IncremarkMath.vue'
12
+ import IncremarkHtmlElement from './IncremarkHtmlElement.vue'
12
13
  import IncremarkDefault from './IncremarkDefault.vue'
13
14
 
14
- defineProps<{
15
+ const props = defineProps<{
15
16
  node: RootContent
16
17
  }>()
17
18
 
@@ -25,14 +26,25 @@ const componentMap: Record<string, Component> = {
25
26
  thematicBreak: IncremarkThematicBreak,
26
27
  math: IncremarkMath,
27
28
  inlineMath: IncremarkMath,
29
+ htmlElement: IncremarkHtmlElement,
28
30
  }
29
31
 
30
32
  function getComponent(type: string): Component {
31
33
  return componentMap[type] || IncremarkDefault
32
34
  }
35
+
36
+ /**
37
+ * 检查是否是 html 节点
38
+ */
39
+ function isHtmlNode(node: RootContent): node is HTML {
40
+ return node.type === 'html'
41
+ }
33
42
  </script>
34
43
 
35
44
  <template>
36
- <component :is="getComponent(node.type)" :node="node" />
45
+ <!-- HTML 节点:渲染为代码块显示源代码 -->
46
+ <pre v-if="isHtmlNode(node)" class="incremark-html-code"><code>{{ (node as HTML).value }}</code></pre>
47
+ <!-- 其他节点:使用对应组件 -->
48
+ <component v-else :is="getComponent(node.type)" :node="node" />
37
49
  </template>
38
50
 
@@ -40,35 +40,3 @@ function getCellContent(cell: TableCell): PhrasingContent[] {
40
40
  </div>
41
41
  </template>
42
42
 
43
- <style scoped>
44
- .incremark-table-wrapper {
45
- overflow-x: auto;
46
- margin: 1em 0;
47
- }
48
-
49
- .incremark-table {
50
- width: 100%;
51
- border-collapse: collapse;
52
- font-size: 14px;
53
- }
54
-
55
- .incremark-table th,
56
- .incremark-table td {
57
- border: 1px solid #ddd;
58
- padding: 10px 14px;
59
- }
60
-
61
- .incremark-table th {
62
- background: #f8f9fa;
63
- font-weight: 600;
64
- }
65
-
66
- .incremark-table tr:nth-child(even) {
67
- background: #fafafa;
68
- }
69
-
70
- .incremark-table tr:hover {
71
- background: #f0f0f0;
72
- }
73
- </style>
74
-
@@ -6,11 +6,3 @@
6
6
  <hr class="incremark-hr" />
7
7
  </template>
8
8
 
9
- <style scoped>
10
- .incremark-hr {
11
- margin: 2em 0;
12
- border: none;
13
- border-top: 2px solid #e5e5e5;
14
- }
15
- </style>
16
-
@@ -15,7 +15,9 @@ export { default as IncremarkBlockquote } from './IncremarkBlockquote.vue'
15
15
  export { default as IncremarkThematicBreak } from './IncremarkThematicBreak.vue'
16
16
  export { default as IncremarkInline } from './IncremarkInline.vue'
17
17
  export { default as IncremarkMath } from './IncremarkMath.vue'
18
+ export { default as IncremarkHtmlElement } from './IncremarkHtmlElement.vue'
18
19
  export { default as IncremarkDefault } from './IncremarkDefault.vue'
20
+ export { default as IncremarkFootnotes } from './IncremarkFootnotes.vue'
19
21
 
20
22
  // 工具组件
21
23
  export { default as AutoScrollContainer } from './AutoScrollContainer.vue'
@@ -0,0 +1,16 @@
1
+ import { inject } from 'vue'
2
+ import { definationsInjectionKey } from './useProvideDefinations'
3
+
4
+ /**
5
+ * support definations and footnoteDefinitions
6
+ * @returns
7
+ */
8
+ export function useDefinationsContext() {
9
+ const definationContext = inject(definationsInjectionKey);
10
+
11
+ if (!definationContext) {
12
+ throw new Error('definationContext not found');
13
+ }
14
+
15
+ return definationContext;
16
+ }