forked from obsidianmd/obsidian-sample-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
233 lines (196 loc) · 8.25 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import { Plugin, setIcon, Notice } from 'obsidian';
export default class LineConverterPlugin extends Plugin {
async onload() {
this.registerMarkdownCodeBlockProcessor('line', async (source, el, ctx) => {
const convertedText = processLineBlock(source);
// Create a code block container with the necessary classes
const codeBlockEl = el.createEl('div', { cls: 'code-block is-loaded' });
// Create the header for the code block (where buttons are placed)
const codeBlockHeader = codeBlockEl.createEl('div', { cls: 'code-block-header' });
// Create the copy button with the 'clickable-icon' class
const copyButton = codeBlockHeader.createEl('div', { cls: 'copy-code-button clickable-icon' });
setIcon(copyButton, 'copy');
// Add event listener to copy the content to the clipboard
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(convertedText).then(() => {
// Provide feedback to the user
copyButton.addClass('mod-copied');
setTimeout(() => copyButton.removeClass('mod-copied'), 1500);
// Show a notification to the user
new Notice('Copied to clipboard!');
});
});
// Obsidian automatically adds the "Edit this block" button, so no need to add it manually
// Create the content area of the code block
const codeBlockContent = codeBlockEl.createEl('div', { cls: 'code-block-content' });
// Create the <pre> and <code> elements to display the converted text
const preEl = codeBlockContent.createEl('pre');
const codeEl = preEl.createEl('code');
codeEl.setText(convertedText);
// Append the code block to the element
el.appendChild(codeBlockEl);
});
}
}
function processLineBlock(source: string): string {
const lines = source.split('\n');
const outputLines = [];
let prevIndentLevel = 0;
let listCounters: number[] = [];
let topLevelCounter = 0;
for (let line of lines) {
// Determine indentation level
const indentMatch = line.match(/^(\t+|\s+)/);
let indentLevel = 0;
if (indentMatch) {
const indent = indentMatch[1];
// For simplicity, consider each tab or 4 spaces as one indent level
const tabCount = (indent.match(/\t/g) || []).length;
const spaceCount = (indent.match(/ /g) || []).length;
indentLevel = tabCount + Math.floor(spaceCount / 4);
// Remove leading indentation
line = line.substring(indent.length);
}
// Now line has no leading indentation
// Handle headings
if (line.startsWith('# ')) {
line = line.replace(/^# (.*)$/, '【 $1 】');
outputLines.push(line);
continue;
} else if (line.startsWith('## ')) {
line = line.replace(/^## (.*)$/, '▋$1');
outputLines.push(line);
continue;
}
// Check for list items
let listItemMatch;
let isListItem = false;
let bullet = '';
let content = '';
let listType = '';
if ((listItemMatch = line.match(/^- \[ \] (.*)/))) {
// Unchecked task
bullet = '🟩 ';
content = listItemMatch[1];
isListItem = true;
listType = 'task';
} else if ((listItemMatch = line.match(/^- \[x\] (.*)/))) {
// Checked task
bullet = '✅ ';
content = listItemMatch[1];
isListItem = true;
listType = 'task';
} else if ((listItemMatch = line.match(/^\d+\.\s+(.*)/))) {
// Ordered list item
bullet = ''; // numbering will be generated
content = listItemMatch[1];
isListItem = true;
listType = 'ordered';
} else if ((listItemMatch = line.match(/^- (.*)/))) {
// Unordered list item
bullet = ''; // numbering will be generated
content = listItemMatch[1];
isListItem = true;
listType = 'unordered';
}
if (isListItem) {
// 处理顶级列表项
if (indentLevel === 0) {
topLevelCounter++;
listCounters = [topLevelCounter];
prevIndentLevel = 0;
} else {
// 处理子列表项
// 确保 listCounters 数组长度与当前缩进级别匹配
while (listCounters.length <= indentLevel) {
listCounters.push(0);
}
// 如果缩进级别改变,调整计数器
if (indentLevel !== prevIndentLevel) {
// 如果缩进更深,添加新的计数器
if (indentLevel > prevIndentLevel) {
listCounters[indentLevel] = 0;
} else {
// 如果缩进更浅,截断数组
listCounters = listCounters.slice(0, indentLevel + 1);
}
}
// 增加当前级别的计数
listCounters[indentLevel]++;
}
// 生成编号
let numbering = listCounters.slice(0, indentLevel + 1).join('.');
// Apply formatting to content
content = applyFormatting(content);
if (bullet === '🟩 ' || bullet === '✅ ') {
// For top-level tasks, include bullet before numbering
numbering = bullet + numbering + '. ' + content;
} else {
numbering = numbering + '. ' + content;
}
// Add to output
outputLines.push(numbering);
prevIndentLevel = indentLevel;
continue;
} else {
// Not a list item
// Reset listCounters and prevIndentLevel if necessary
listCounters = [];
prevIndentLevel = 0;
// Apply formatting replacements to the line
line = applyFormatting(line);
outputLines.push(line);
}
}
return outputLines.join('\n');
}
function applyFormatting(text: string): string {
// Define the patterns and their replacements in order
const patterns = [
{ regex: /\*\*(.*?)\*\*/, replacement: ' *$1* ' }, // bold
{ regex: /(^|[^*])\*(.*?)\*(?!\*)/, replacement: '$1_$2_' }, // italic
{ regex: /~~(.*?)~~/, replacement: ' ~$1~ ' }, // strike
{ regex: /==(.*?)==/, replacement: ' `$1` ' }, // emphasize
{ regex: /`(.*?)`/, replacement: ' {$1} ' }, // quote
];
let result = '';
let remainingText = text;
while (remainingText.length > 0) {
let earliestMatch:
| { pattern: any; match: RegExpExecArray; index: number }
| null = null;
let earliestIndex = remainingText.length;
// Find the earliest match among the patterns
for (let pattern of patterns) {
pattern.regex.lastIndex = 0; // Reset regex index
let match = pattern.regex.exec(remainingText);
if (match && match.index < earliestIndex) {
earliestMatch = {
pattern: pattern,
match: match,
index: match.index,
};
earliestIndex = match.index;
}
}
if (earliestMatch) {
// Append text before the match
result += remainingText.slice(0, earliestMatch.index);
// Apply the replacement
let replacedText = earliestMatch.match[0].replace(
earliestMatch.pattern.regex,
earliestMatch.pattern.replacement
);
result += replacedText;
// Update remainingText
remainingText = remainingText.slice(
earliestMatch.index + earliestMatch.match[0].length
);
} else {
// No more matches, append the rest of the text
result += remainingText;
break;
}
}
return result;
}