Skip to content

Commit 89e7103

Browse files
authored
Merge pull request #11 from ut-code/load-markdown
Load markdown
2 parents cb247a4 + 01a8925 commit 89e7103

4 files changed

Lines changed: 357 additions & 33 deletions

File tree

src/App.tsx

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import parse, { Element, HTMLReactParserOptions } from "html-react-parser";
44
// import rehypeKatex from "rehype-katex";
55
// import remarkMath from "remark-math";
66
import Tippy from "@tippyjs/react";
7-
import markdownLink from "/hoge.md?url";
7+
// import markdownLink from "/hoge.md?url";
88
import { ExtractDefinitions } from "./MDToDefinitions";
99
import { MDToHTML } from "./MDToHTML";
1010
import { replaceExternalSyntax } from "./external-syntax";
1111
import { ExtractPDF } from "./extractPDF";
1212
import pdfFile from "/chibutsu_nyumon.pdf";
1313
import Textarea from "@mui/joy/Textarea";
14+
import { Button } from "@mui/material";
1415

1516
import "katex/dist/katex.min.css";
1617
import "tippy.js/dist/tippy.css";
18+
import UploadMarkdown from "./uploadMarkdown";
19+
import UploadImage from "./uploadImage";
1720

1821
type positionInfo = null | { top: number; left: number };
1922

@@ -28,29 +31,33 @@ export default function App() {
2831

2932
// ドラッグして直接参照できる機能の部分
3033
const [inputPosition, setInputPosition] = useState<positionInfo>(null); // ドラッグされた位置
31-
// const [selectedText, setSelectedText] = useState(""); // ドラッグされた文章 // ...unused
3234
const [inputValue, setInputValue] = useState("");
33-
const [textAreaValue, setTextAreaValue] = useState("");
3435
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
36+
const [visualize, setVisualize] = useState(true); // テキストエリアを表示にするか非表示にするか
37+
const [fileContent, setFileContent] = useState<string>("");
38+
const [imageData, setImageData] = useState<string>("");
3539

3640
// get markdown
41+
// useEffect(() => {
42+
// fetch(markdownLink)
43+
// .then((res) => res.text())
44+
// .then((t) => setMarkdown(t))
45+
// .catch((err) => console.error("Error fetching Hoge.md:", err));
46+
// }, []);
47+
48+
useEffect(() => {
49+
setMarkdown(fileContent);
50+
}, [fileContent]);
51+
3752
useEffect(() => {
38-
fetch(markdownLink)
39-
.then((res) => res.text())
40-
.then((t) => setMarkdown(t))
41-
.catch((err) => console.error("Error fetching Hoge.md:", err));
42-
}, []);
53+
localStorage.setItem("item", markdown);
54+
}, [markdown]); // markdownの内容が変わるたびにlocalStorageに保存。
4355

4456
// use markdown (separation is necessary because it's async)
45-
useEffect(() => void insideUseEffect(), [markdown + textAreaValue]);
57+
useEffect(() => void insideUseEffect(), [markdown]);
4658
async function insideUseEffect() {
4759
// prepare dictionary
48-
let d = ExtractDefinitions(
49-
markdown + textAreaValue,
50-
opts.prefix,
51-
opts.suffix,
52-
);
53-
console.log(markdown + textAreaValue);
60+
let d = ExtractDefinitions(markdown, opts.prefix, opts.suffix);
5461
const newd = new Map<string, string>();
5562
const promises: Promise<Map<string, string>>[] = [];
5663
d.forEach((v, k) => {
@@ -64,7 +71,7 @@ export default function App() {
6471
// prepare HTML
6572
var md;
6673
try {
67-
md = replaceExternalSyntax(markdown);
74+
md = replaceExternalSyntax(markdown.replace(/!define[\s\S]*$/m, "")); // !define以下をすべて取り去る。
6875
} catch (e: any) {
6976
md = e.toString();
7077
}
@@ -111,6 +118,10 @@ export default function App() {
111118
// console.log(inputValue) 入力された内容がここに入る。
112119
};
113120

121+
const handleImageChange = (content: string) => {
122+
setImageData(content);
123+
};
124+
114125
const handleTextAreaFocus = () => {
115126
setIsTextAreaFocused(true);
116127
};
@@ -119,12 +130,76 @@ export default function App() {
119130
setIsTextAreaFocused(false);
120131
};
121132

133+
// テキストファイルを保存する
134+
const saveFile = () => {
135+
const blob = new Blob([markdown], {
136+
type: ".md, text/markdown",
137+
});
138+
const link = document.createElement("a");
139+
link.href = URL.createObjectURL(blob);
140+
link.download = localStorage.getItem("filename") ?? "hoge.md"; // localStorage上に保存したファイル名を使う。
141+
link.click();
142+
};
143+
122144
return (
123145
<>
124-
<ConvertMarkdown dictionary={dict} html={html} opts={opts} />
146+
<div className="save_container">
147+
<div className="upload_save">
148+
<UploadMarkdown onFileContentChange={setFileContent} />
149+
<Button variant="text" onClick={saveFile}>
150+
保存
151+
</Button>
152+
</div>
153+
<div className="upload_save">
154+
<UploadImage onImageChange={handleImageChange} />
155+
</div>
156+
</div>
157+
{visualize == false && (
158+
<>
159+
<div className="upload_save">
160+
<Button
161+
variant="text"
162+
onClick={() => {
163+
setVisualize(true);
164+
}}
165+
>
166+
編集画面の表示
167+
</Button>
168+
</div>
169+
<div className="wrapper_false">
170+
<ConvertMarkdown dictionary={dict} html={html} opts={opts} />
171+
</div>
172+
</>
173+
)}
174+
<div>{imageData}</div>
175+
{visualize == true && (
176+
<>
177+
<div className="upload_save">
178+
<Button
179+
variant="text"
180+
onClick={() => {
181+
setVisualize(false);
182+
}}
183+
>
184+
編集画面の非表示
185+
</Button>
186+
</div>
187+
<div className="wrapper_true">
188+
<div className="convert_markdown">
189+
<ConvertMarkdown dictionary={dict} html={html} opts={opts} />
190+
</div>
191+
<textarea
192+
value={markdown}
193+
onChange={(event) => {
194+
setMarkdown(event.target.value);
195+
}}
196+
placeholder="編集画面"
197+
/>
198+
</div>
199+
</>
200+
)}
125201
<ExtractPDF pdfName={pdfFile} opts={opts} />
126202
{/* ドラッグして参照する部分 */}
127-
<Textarea value={textAreaValue} placeholder="結果" minRows={10} />
128203
{inputPosition && (
129204
<>
130205
<Textarea
@@ -140,9 +215,7 @@ export default function App() {
140215
/>
141216
<button
142217
onClick={() =>
143-
setTextAreaValue(
144-
(textAreaValue) => textAreaValue + "\n" + inputValue,
145-
)
218+
setMarkdown((markdown) => markdown + "\n" + inputValue + "\n")
146219
}
147220
style={{
148221
position: "absolute",
@@ -160,6 +233,7 @@ export default function App() {
160233

161234
// this uses given dictionary as the source to extract definition from,
162235
// and given html to render the main note.
236+
163237
function ConvertMarkdown({
164238
dictionary,
165239
html,
@@ -170,14 +244,16 @@ function ConvertMarkdown({
170244
}) {
171245
let parsing = html.split("\n");
172246

173-
// this is O(n**2). reduce the order if you can.
247+
dictionary = new Map(
248+
[...dictionary.entries()].sort((a, b) => a[0].length - b[0].length),
249+
); // Sort dictionary entries by word length to avoid overlapping replacements
250+
251+
// Replace words with tooltip-enabled spans
174252
dictionary.forEach((_def: string, word: string) => {
175253
let idx = 0;
176254
for (const line of parsing) {
177-
// remove popup of the definition itself, because it looks ugly
178-
// I hard-coded the assumption that a definition will turn into h2. if you got any better way to do this, do that.
255+
// Skip lines that are part of the definition to avoid replacing inside the definition itself
179256
if (!line.includes(`<h2>${word}</h2>`)) {
180-
// make sure ${word} is the first attribute of class; otherwise the word replacement below will fail.
181257
parsing[idx] = line.replaceAll(
182258
word,
183259
`<span class="${word} underline">${word}</span>`,
@@ -186,37 +262,52 @@ function ConvertMarkdown({
186262
idx++;
187263
}
188264
});
265+
189266
let parsedHtml = parsing.join("\n");
190267

191268
const options: HTMLReactParserOptions = {
192269
replace(domNode) {
193270
if (!(domNode instanceof Element)) {
194271
return domNode;
195272
}
196-
// domNode の最初の class 属性を取り出す。 (indexError でなく undefined になるため、[0] は安全)
273+
274+
const tagName = domNode.tagName;
275+
276+
// Handle images
277+
if (tagName === "img") {
278+
const src = domNode.attribs?.src;
279+
const alt = domNode.attribs?.alt;
280+
return (
281+
<img src={src} alt={alt || "image"} style={{ maxWidth: "100%" }} />
282+
);
283+
}
284+
197285
const word: string | undefined = domNode.attribs?.class?.split(" ")[0];
198-
// HTML 的には多分動くが、気持ち悪いので最初の class 属性 = word を排除
199286
const newClass: string = domNode.attribs?.class
200287
?.split(" ")
201288
.slice(0)
202289
.join(" ");
203-
// 与えられたノードが Element であり、その class 属性が undefined または空文字列でなく、 dictionary 内のいずれかの単語と一致するかどうかを確認
290+
291+
// Handle words that should show tooltips
204292
if (
205293
domNode instanceof Element &&
206294
domNode.attribs?.class &&
207295
dictionary.has(word)
208296
) {
209297
return (
210-
// dictionary.get(word) is an html and therefore must not be used directly
211-
<Tippy content={parse(dictionary.get(word) || "")}>
298+
<Tippy
299+
content={parse(dictionary.get(word) || "")}
300+
className="markdown_tippy"
301+
>
212302
<span className={newClass}>{word}</span>
213303
</Tippy>
214304
);
215305
}
216-
// 条件を満たさない場合は、元のノードをそのまま返す
217-
return domNode;
306+
307+
return domNode; // Return the domNode unchanged if no special handling is needed
218308
},
219309
};
220310

221-
return <>{parse(parsedHtml, options)}</>; // パースされた HTML を返す
311+
return <>{parse(parsedHtml, options)}</>; // パースされた HTML を返す
312+
222313
}

src/index.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
-webkit-text-size-adjust: 100%;
1515
}
1616

17+
#root {
18+
width: 100%;
19+
}
20+
1721
a {
1822
font-weight: 500;
1923
color: #646cff;
@@ -91,3 +95,55 @@ button:focus-visible {
9195
border-bottom: 1.5px dotted rgb(0, 144, 19);
9296
display: inline-block;
9397
}
98+
99+
.wrapper_true {
100+
display: flex;
101+
width: 100%;
102+
}
103+
104+
.wrapper_true div {
105+
width: 47%;
106+
margin: 0.5%;
107+
padding: 1%;
108+
height: 660px;
109+
border: 1px solid black;
110+
border-radius: 3px;
111+
overflow: scroll;
112+
}
113+
114+
.wrapper_true textarea {
115+
width: 47%;
116+
height: 660px;
117+
margin: 0.5%;
118+
padding: 1%;
119+
font-family: inherit;
120+
}
121+
122+
.wrapper_false {
123+
margin: 0.5%;
124+
padding: 1%;
125+
height: 660px;
126+
border: 1px solid black;
127+
border-radius: 3px;
128+
overflow: scroll;
129+
}
130+
131+
.upload_save {
132+
display: inline;
133+
border: 1px solid black;
134+
border-radius: 3px;
135+
margin: 10px;
136+
padding: 10px;
137+
}
138+
139+
.save_container {
140+
display: flex;
141+
}
142+
143+
.convert_markdown img {
144+
max-width: 100%;
145+
}
146+
147+
.markdown_tippy img {
148+
max-width: 320px;
149+
}

0 commit comments

Comments
 (0)