Skip to content

Commit 8785d86

Browse files
authored
Merge pull request #85 from eric861129/dev
refactor: split useMarkdownEditor into specialized hooks and bump ver…
2 parents 7ca91dc + 01b4ecd commit 8785d86

11 files changed

Lines changed: 264 additions & 149 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.8] - 2026-01-09
9+
10+
### Refactored
11+
- **Hook Architecture**:
12+
- Decomposed the monolithic `useMarkdownEditor` into specialized hooks for better maintainability and separation of concerns.
13+
- Added `hooks/useEditorState.ts`: Manages core content and parsing state.
14+
- Added `hooks/useDocxExport.ts`: Handles document generation and file IO.
15+
- Added `hooks/useSyncScroll.ts`: Encapsulates scroll synchronization logic.
16+
- Added `hooks/useWordCount.ts`: Optimized word counting with `useMemo`.
17+
818
## [1.2.7] - 2026-01-09
919

1020
### Added

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# BookPublisher MD2Docx | v1.2.7
1+
# BookPublisher MD2Docx | v1.2.8
22

33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4-
[![Version](https://img.shields.io/badge/version-1.2.7-blue.svg)](https://github.com/eric861129/BookPublisher_MD2Doc)
4+
[![Version](https://img.shields.io/badge/version-1.2.8-blue.svg)](https://github.com/eric861129/BookPublisher_MD2Doc)
55

66
[🇹🇼 中文](README.md) | [🇺🇸 English](README_EN.md)
77

README_EN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# BookPublisher MD2Docx | v1.2.7
1+
# BookPublisher MD2Docx | v1.2.8
22

33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4-
[![Version](https://img.shields.io/badge/version-1.2.7-blue.svg)](https://github.com/eric861129/BookPublisher_MD2Doc)
4+
[![Version](https://img.shields.io/badge/version-1.2.8-blue.svg)](https://github.com/eric861129/BookPublisher_MD2Doc)
55

66
[🇹🇼 中文](README.md) | [🇺🇸 English](README_EN.md)
77

constants/defaultContent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ ${BT}${BT}${BT}
8888
${BT}${BT}${BT}json:no-ln
8989
{
9090
"name": "book-publisher",
91-
"version": "1.2.7",
91+
"version": "1.2.8",
9292
"private": true
9393
}
9494
${BT}${BT}${BT}
@@ -231,7 +231,7 @@ ${BT}${BT}${BT}
231231
Use ${BT}json:no-ln${BT} or ${BT}:plain${BT} syntax. Ideal for short config files or examples where line numbers aren't needed:
232232
233233
"name": "book-publisher",
234-
"version": "1.2.7",
234+
"version": "1.2.8",
235235
"private": true
236236
}
237237
${BT}${BT}${BT}

constants/meta.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@
77
*/
88

99
export const APP_VERSION = __APP_VERSION__;
10+
11+
export const PAGE_SIZES = [
12+
{ name: "tech", width: 17, height: 23 },
13+
{ name: "a4", width: 21, height: 29.7 },
14+
{ name: "a5", width: 14.8, height: 21 },
15+
{ name: "b5", width: 17.6, height: 25 },
16+
];

hooks/useDocxExport.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useState } from 'react';
2+
import saveAs from 'file-saver';
3+
import { generateDocx } from '../services/docxGenerator';
4+
import { ParsedBlock, DocumentMeta } from '../services/types';
5+
import { PAGE_SIZES } from '../constants/meta';
6+
7+
interface UseDocxExportProps {
8+
content: string;
9+
parsedBlocks: ParsedBlock[];
10+
documentMeta: DocumentMeta;
11+
}
12+
13+
export const useDocxExport = ({ content, parsedBlocks, documentMeta }: UseDocxExportProps) => {
14+
const [isGenerating, setIsGenerating] = useState(false);
15+
const [selectedSizeIndex, setSelectedSizeIndex] = useState(0);
16+
17+
// DOCX Download
18+
const handleDownload = async () => {
19+
if (parsedBlocks.length === 0) return;
20+
setIsGenerating(true);
21+
try {
22+
const sizeConfig = PAGE_SIZES[selectedSizeIndex];
23+
const blob = await generateDocx(parsedBlocks, {
24+
widthCm: sizeConfig.width,
25+
heightCm: sizeConfig.height,
26+
showLineNumbers: true, // Default to true for technical books
27+
meta: documentMeta
28+
});
29+
30+
const safeTitle = documentMeta.title
31+
? documentMeta.title.replace(/[\\/:*?"<>|]/g, '_')
32+
: "Professional_Manuscript";
33+
34+
saveAs(blob, `${safeTitle}.docx`);
35+
} catch (error) {
36+
console.error("Word Generation Failed:", error);
37+
alert("轉檔失敗,請確認內容格式是否正確。");
38+
} finally {
39+
setIsGenerating(false);
40+
}
41+
};
42+
43+
// Markdown Export
44+
const handleExportMarkdown = () => {
45+
if (!content) return;
46+
try {
47+
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
48+
49+
const safeTitle = documentMeta.title
50+
? documentMeta.title.replace(/[\\/:*?"<>|]/g, '_')
51+
: "manuscript";
52+
53+
saveAs(blob, `${safeTitle}.md`);
54+
} catch (error) {
55+
console.error("Markdown Export Failed:", error);
56+
alert("匯出失敗");
57+
}
58+
};
59+
60+
return {
61+
isGenerating,
62+
selectedSizeIndex,
63+
setSelectedSizeIndex,
64+
handleDownload,
65+
handleExportMarkdown,
66+
pageSizes: PAGE_SIZES
67+
};
68+
};

hooks/useEditorState.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useState, useEffect } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { parseMarkdown } from '../services/markdownParser';
4+
import { ParsedBlock, DocumentMeta } from '../services/types';
5+
import { INITIAL_CONTENT_ZH, INITIAL_CONTENT_EN } from '../constants/defaultContent';
6+
7+
export const useEditorState = () => {
8+
const { t, i18n } = useTranslation();
9+
const language = i18n.language.split('-')[0];
10+
11+
const getInitialContent = (lang: string) => lang.startsWith('zh') ? INITIAL_CONTENT_ZH : INITIAL_CONTENT_EN;
12+
13+
const [content, setContent] = useState(() => {
14+
return localStorage.getItem('draft_content') || getInitialContent(i18n.language);
15+
});
16+
17+
const [parsedBlocks, setParsedBlocks] = useState<ParsedBlock[]>([]);
18+
const [documentMeta, setDocumentMeta] = useState<DocumentMeta>({});
19+
20+
// Parsing & Auto-save (Debounced)
21+
useEffect(() => {
22+
const timer = setTimeout(() => {
23+
try {
24+
const { blocks, meta } = parseMarkdown(content);
25+
setParsedBlocks(blocks);
26+
setDocumentMeta(meta);
27+
localStorage.setItem('draft_content', content);
28+
} catch (e) {
29+
console.error("Markdown parsing error:", e);
30+
}
31+
}, 300);
32+
33+
return () => clearTimeout(timer);
34+
}, [content]);
35+
36+
// Language Toggle Logic
37+
const toggleLanguage = () => {
38+
const nextLang = i18n.language.startsWith('zh') ? 'en' : 'zh';
39+
40+
if (confirm(t('switchLangConfirm'))) {
41+
i18n.changeLanguage(nextLang);
42+
setContent(getInitialContent(nextLang));
43+
localStorage.removeItem('draft_content');
44+
}
45+
};
46+
47+
// Reset Logic
48+
const resetToDefault = () => {
49+
if (confirm(t('resetConfirm'))) {
50+
setContent(getInitialContent(i18n.language));
51+
localStorage.removeItem('draft_content');
52+
}
53+
};
54+
55+
return {
56+
content,
57+
setContent,
58+
parsedBlocks,
59+
documentMeta,
60+
language,
61+
toggleLanguage,
62+
resetToDefault,
63+
t // Export translation helper if needed
64+
};
65+
};

0 commit comments

Comments
 (0)