@@ -4,16 +4,19 @@ import parse, { Element, HTMLReactParserOptions } from "html-react-parser";
44// import rehypeKatex from "rehype-katex";
55// import remarkMath from "remark-math";
66import Tippy from "@tippyjs/react" ;
7- import markdownLink from "/hoge.md?url" ;
7+ // import markdownLink from "/hoge.md?url";
88import { ExtractDefinitions } from "./MDToDefinitions" ;
99import { MDToHTML } from "./MDToHTML" ;
1010import { replaceExternalSyntax } from "./external-syntax" ;
1111import { ExtractPDF } from "./extractPDF" ;
1212import pdfFile from "/chibutsu_nyumon.pdf" ;
1313import Textarea from "@mui/joy/Textarea" ;
14+ import { Button } from "@mui/material" ;
1415
1516import "katex/dist/katex.min.css" ;
1617import "tippy.js/dist/tippy.css" ;
18+ import UploadMarkdown from "./uploadMarkdown" ;
19+ import UploadImage from "./uploadImage" ;
1720
1821type 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 ( / ! d e f i n e [ \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+
163237function 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}
0 commit comments