mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-09-14 05:47:38 +02:00
Refactor and enhance type safety across various modules
- Updated function parameters and return types in `page-preview.ts`, `pdf-decrypt.ts`, and `pymupdf-loader.ts` for improved type safety. - Introduced type definitions for `CpdfInstance`, `PyMuPDFInstance`, and other related types to ensure better type checking. - Enhanced error handling in `sanitize.ts` by creating a utility function for error messages. - Removed unnecessary type assertions and improved type inference in `editor.ts`, `serialization.ts`, and `tools.test.ts`. - Added type definitions for markdown-it plugins to improve compatibility and type safety. - Enforced stricter TypeScript settings by enabling `noImplicitAny` in `tsconfig.json`. - Cleaned up test files by refining type assertions and ensuring consistency in type usage.
This commit is contained in:
+123
-71
@@ -29,8 +29,14 @@ import taskLists from 'markdown-it-task-lists';
|
||||
import anchor from 'markdown-it-anchor';
|
||||
import tocDoneRight from 'markdown-it-toc-done-right';
|
||||
import { applyTranslations } from '../i18n/i18n';
|
||||
|
||||
|
||||
import type {
|
||||
WindowWithLucide,
|
||||
WindowWithI18next,
|
||||
MarkdownItOptions,
|
||||
MarkdownItToken,
|
||||
MarkdownItRendererSelf,
|
||||
MarkdownItRenderRule,
|
||||
} from '@/types';
|
||||
|
||||
// Register highlight.js languages
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
@@ -66,18 +72,7 @@ export interface MarkdownEditorOptions {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export interface MarkdownItOptions {
|
||||
/** Enable HTML tags in source */
|
||||
html: boolean;
|
||||
/** Convert '\n' in paragraphs into <br> */
|
||||
breaks: boolean;
|
||||
/** Autoconvert URL-like text to links */
|
||||
linkify: boolean;
|
||||
/** Enable some language-neutral replacement + quotes beautification */
|
||||
typographer: boolean;
|
||||
/** Highlight function for fenced code blocks */
|
||||
highlight?: (str: string, lang: string) => string;
|
||||
}
|
||||
export type { MarkdownItOptions } from '@/types';
|
||||
|
||||
const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor
|
||||
|
||||
@@ -277,7 +272,7 @@ export class MarkdownEditor {
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
typographer: true,
|
||||
};
|
||||
|
||||
constructor(container: HTMLElement, options: MarkdownEditorOptions) {
|
||||
@@ -303,22 +298,36 @@ export class MarkdownEditor {
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
});
|
||||
this.mermaidInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private configureLinkRenderer(): void {
|
||||
// Override link renderer to add target="_blank" and rel="noopener"
|
||||
const defaultRender = this.md.renderer.rules.link_open ||
|
||||
((tokens: any[], idx: number, options: any, _env: any, self: any) => self.renderToken(tokens, idx, options));
|
||||
const existingRule = this.md.renderer.rules.link_open;
|
||||
const defaultRender: MarkdownItRenderRule = existingRule
|
||||
? (existingRule as unknown as MarkdownItRenderRule)
|
||||
: (
|
||||
tokens: MarkdownItToken[],
|
||||
idx: number,
|
||||
options: MarkdownItOptions,
|
||||
env: unknown,
|
||||
self: MarkdownItRendererSelf
|
||||
) => self.renderToken(tokens, idx, options);
|
||||
|
||||
this.md.renderer.rules.link_open = (tokens: any[], idx: number, options: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
this.md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx] as unknown as MarkdownItToken;
|
||||
token.attrSet('target', '_blank');
|
||||
token.attrSet('rel', 'noopener noreferrer');
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
return defaultRender(
|
||||
tokens as unknown as MarkdownItToken[],
|
||||
idx,
|
||||
options as unknown as MarkdownItOptions,
|
||||
env as unknown,
|
||||
self as unknown as MarkdownItRendererSelf
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,12 +426,11 @@ export class MarkdownEditor {
|
||||
this.applyI18n();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof (window as any).lucide !== 'undefined') {
|
||||
(window as any).lucide.createIcons();
|
||||
if (typeof (window as WindowWithLucide).lucide !== 'undefined') {
|
||||
(window as WindowWithLucide).lucide?.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Editor input
|
||||
this.editor?.addEventListener('input', () => {
|
||||
@@ -441,9 +449,13 @@ export class MarkdownEditor {
|
||||
this.editor?.addEventListener('scroll', () => {
|
||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||
this.isSyncing = true;
|
||||
const scrollPercentage = this.editor.scrollTop / (this.editor.scrollHeight - this.editor.clientHeight);
|
||||
this.preview.scrollTop = scrollPercentage * (this.preview.scrollHeight - this.preview.clientHeight);
|
||||
setTimeout(() => this.isSyncing = false, 10);
|
||||
const scrollPercentage =
|
||||
this.editor.scrollTop /
|
||||
(this.editor.scrollHeight - this.editor.clientHeight);
|
||||
this.preview.scrollTop =
|
||||
scrollPercentage *
|
||||
(this.preview.scrollHeight - this.preview.clientHeight);
|
||||
setTimeout(() => (this.isSyncing = false), 10);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -451,9 +463,13 @@ export class MarkdownEditor {
|
||||
this.preview?.addEventListener('scroll', () => {
|
||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||
this.isSyncing = true;
|
||||
const scrollPercentage = this.preview.scrollTop / (this.preview.scrollHeight - this.preview.clientHeight);
|
||||
this.editor.scrollTop = scrollPercentage * (this.editor.scrollHeight - this.editor.clientHeight);
|
||||
setTimeout(() => this.isSyncing = false, 10);
|
||||
const scrollPercentage =
|
||||
this.preview.scrollTop /
|
||||
(this.preview.scrollHeight - this.preview.clientHeight);
|
||||
this.editor.scrollTop =
|
||||
scrollPercentage *
|
||||
(this.editor.scrollHeight - this.editor.clientHeight);
|
||||
setTimeout(() => (this.isSyncing = false), 10);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -474,22 +490,30 @@ export class MarkdownEditor {
|
||||
});
|
||||
|
||||
// Settings modal close
|
||||
document.getElementById('mdCloseSettings')?.addEventListener('click', () => {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
document.getElementById('mdSettingsModal')?.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).classList.contains('md-editor-modal-overlay')) {
|
||||
document
|
||||
.getElementById('mdCloseSettings')
|
||||
?.addEventListener('click', () => {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
document
|
||||
.getElementById('mdSettingsModal')
|
||||
?.addEventListener('click', (e) => {
|
||||
if (
|
||||
(e.target as HTMLElement).classList.contains(
|
||||
'md-editor-modal-overlay'
|
||||
)
|
||||
) {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Settings checkboxes
|
||||
document.getElementById('mdOptHtml')?.addEventListener('change', (e) => {
|
||||
@@ -507,10 +531,12 @@ export class MarkdownEditor {
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
document.getElementById('mdOptTypographer')?.addEventListener('change', (e) => {
|
||||
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
document
|
||||
.getElementById('mdOptTypographer')
|
||||
?.addEventListener('change', (e) => {
|
||||
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
// Preset selector
|
||||
document.getElementById('mdPreset')?.addEventListener('change', (e) => {
|
||||
@@ -549,7 +575,8 @@ export class MarkdownEditor {
|
||||
const start = this.editor!.selectionStart;
|
||||
const end = this.editor!.selectionEnd;
|
||||
const value = this.editor!.value;
|
||||
this.editor!.value = value.substring(0, start) + ' ' + value.substring(end);
|
||||
this.editor!.value =
|
||||
value.substring(0, start) + ' ' + value.substring(end);
|
||||
this.editor!.selectionStart = this.editor!.selectionEnd = start + 2;
|
||||
this.updatePreview();
|
||||
}
|
||||
@@ -563,18 +590,37 @@ export class MarkdownEditor {
|
||||
|
||||
// Update options based on preset
|
||||
if (preset === 'commonmark') {
|
||||
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
|
||||
this.mdOptions = {
|
||||
html: false,
|
||||
breaks: false,
|
||||
linkify: false,
|
||||
typographer: false,
|
||||
};
|
||||
} else if (preset === 'zero') {
|
||||
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
|
||||
this.mdOptions = {
|
||||
html: false,
|
||||
breaks: false,
|
||||
linkify: false,
|
||||
typographer: false,
|
||||
};
|
||||
} else {
|
||||
this.mdOptions = { html: true, breaks: false, linkify: true, typographer: true };
|
||||
this.mdOptions = {
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Update UI checkboxes
|
||||
(document.getElementById('mdOptHtml') as HTMLInputElement).checked = this.mdOptions.html;
|
||||
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked = this.mdOptions.breaks;
|
||||
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked = this.mdOptions.linkify;
|
||||
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked = this.mdOptions.typographer;
|
||||
(document.getElementById('mdOptHtml') as HTMLInputElement).checked =
|
||||
this.mdOptions.html;
|
||||
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked =
|
||||
this.mdOptions.breaks;
|
||||
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked =
|
||||
this.mdOptions.linkify;
|
||||
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked =
|
||||
this.mdOptions.typographer;
|
||||
|
||||
this.updateMarkdownIt();
|
||||
}
|
||||
@@ -588,7 +634,6 @@ export class MarkdownEditor {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private createMarkdownIt(): MarkdownIt {
|
||||
// Use preset if commonmark or zero
|
||||
let md: MarkdownIt;
|
||||
@@ -604,28 +649,31 @@ export class MarkdownEditor {
|
||||
highlight: (str: string, lang: string) => {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
|
||||
return hljs.highlight(str, {
|
||||
language: lang,
|
||||
ignoreIllegals: true,
|
||||
}).value;
|
||||
} catch {
|
||||
// Fall through to default
|
||||
}
|
||||
}
|
||||
return ''; // Use external default escaping
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Apply plugins only for default preset (plugins may not work well with commonmark/zero)
|
||||
if (this.currentPreset === 'default') {
|
||||
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
|
||||
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
|
||||
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
|
||||
.use(deflist) // Definition lists
|
||||
.use(abbr) // Abbreviations: *[abbr]: full text
|
||||
.use(emoji) // Emoji: :smile: -> 😄
|
||||
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
|
||||
.use(mark) // Marked text: ==text== -> <mark>text</mark>
|
||||
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
|
||||
.use(anchor, { permalink: false }) // Header anchors
|
||||
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
|
||||
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
|
||||
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
|
||||
.use(deflist) // Definition lists
|
||||
.use(abbr) // Abbreviations: *[abbr]: full text
|
||||
.use(emoji) // Emoji: :smile: -> 😄
|
||||
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
|
||||
.use(mark) // Marked text: ==text== -> <mark>text</mark>
|
||||
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
|
||||
.use(anchor, { permalink: false }) // Header anchors
|
||||
.use(tocDoneRight); // Table of contents: ${toc}
|
||||
}
|
||||
|
||||
@@ -650,7 +698,9 @@ export class MarkdownEditor {
|
||||
private async renderMermaidDiagrams(): Promise<void> {
|
||||
if (!this.preview) return;
|
||||
|
||||
const mermaidBlocks = this.preview.querySelectorAll('pre > code.language-mermaid');
|
||||
const mermaidBlocks = this.preview.querySelectorAll(
|
||||
'pre > code.language-mermaid'
|
||||
);
|
||||
|
||||
for (let i = 0; i < mermaidBlocks.length; i++) {
|
||||
const block = mermaidBlocks[i] as HTMLElement;
|
||||
@@ -948,14 +998,16 @@ ${content}
|
||||
applyTranslations();
|
||||
|
||||
// Special handling for select options (data-i18n on options doesn't work with applyTranslations)
|
||||
const presetSelect = document.getElementById('mdPreset') as HTMLSelectElement;
|
||||
const presetSelect = document.getElementById(
|
||||
'mdPreset'
|
||||
) as HTMLSelectElement;
|
||||
if (presetSelect) {
|
||||
const options = presetSelect.querySelectorAll('option[data-i18n]');
|
||||
options.forEach((option) => {
|
||||
const key = option.getAttribute('data-i18n');
|
||||
if (key) {
|
||||
// Use i18next directly for option text
|
||||
const translated = (window as any).i18next?.t(key);
|
||||
const translated = (window as WindowWithI18next).i18next?.t(key);
|
||||
if (translated && translated !== key) {
|
||||
option.textContent = translated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user