markdown-to-document-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +335 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/services/ContentValidator.d.ts +50 -0
- package/dist/services/ContentValidator.d.ts.map +1 -0
- package/dist/services/ContentValidator.js +385 -0
- package/dist/services/ContentValidator.js.map +1 -0
- package/dist/services/MarkdownPreprocessor.d.ts +29 -0
- package/dist/services/MarkdownPreprocessor.d.ts.map +1 -0
- package/dist/services/MarkdownPreprocessor.js +168 -0
- package/dist/services/MarkdownPreprocessor.js.map +1 -0
- package/dist/services/PandocService.d.ts +82 -0
- package/dist/services/PandocService.d.ts.map +1 -0
- package/dist/services/PandocService.js +285 -0
- package/dist/services/PandocService.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/validators.d.ts +31 -0
- package/dist/types/validators.d.ts.map +1 -0
- package/dist/types/validators.js +5 -0
- package/dist/types/validators.js.map +1 -0
- package/dist/utils/common.d.ts +19 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +73 -0
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/constants.d.ts +24 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +156 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/fileUtils.d.ts +17 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +70 -0
- package/dist/utils/fileUtils.js.map +1 -0
- package/dist/utils/markdownUtils.d.ts +27 -0
- package/dist/utils/markdownUtils.d.ts.map +1 -0
- package/dist/utils/markdownUtils.js +171 -0
- package/dist/utils/markdownUtils.js.map +1 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown to Document CLI - Main Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import type { ConversionOptions, ConversionResult } from './types/index.js';
|
|
5
|
+
export declare class MarkdownToDocument {
|
|
6
|
+
private pandocService;
|
|
7
|
+
private preprocessor;
|
|
8
|
+
private validator;
|
|
9
|
+
constructor(pandocPath?: string);
|
|
10
|
+
/**
|
|
11
|
+
* Initialize and check dependencies
|
|
12
|
+
*/
|
|
13
|
+
initialize(): Promise<{
|
|
14
|
+
success: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Convert markdown to EPUB/PDF
|
|
19
|
+
*/
|
|
20
|
+
convert(options: ConversionOptions): Promise<ConversionResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Get Pandoc installation instructions
|
|
23
|
+
*/
|
|
24
|
+
static getInstallInstructions(): string;
|
|
25
|
+
}
|
|
26
|
+
export * from './types/index.js';
|
|
27
|
+
export * from './types/validators.js';
|
|
28
|
+
export { PandocService } from './services/PandocService.js';
|
|
29
|
+
export { MarkdownPreprocessor } from './services/MarkdownPreprocessor.js';
|
|
30
|
+
export { ContentValidator } from './services/ContentValidator.js';
|
|
31
|
+
export { DEFAULT_CONFIG } from './utils/constants.js';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAO5E,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAmB;gBAExB,UAAU,CAAC,EAAE,MAAM;IAM/B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAkBjE;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuIpE;;OAEG;IACH,MAAM,CAAC,sBAAsB,IAAI,MAAM;CAG1C;AAGD,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown to Document CLI - Main Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import { PandocService } from './services/PandocService.js';
|
|
5
|
+
import { MarkdownPreprocessor } from './services/MarkdownPreprocessor.js';
|
|
6
|
+
import { ContentValidator } from './services/ContentValidator.js';
|
|
7
|
+
import { ensureDirectory, sanitizeFilename, getTempDir } from './utils/fileUtils.js';
|
|
8
|
+
import { Logger } from './utils/common.js';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
export class MarkdownToDocument {
|
|
12
|
+
pandocService;
|
|
13
|
+
preprocessor;
|
|
14
|
+
validator;
|
|
15
|
+
constructor(pandocPath) {
|
|
16
|
+
this.pandocService = new PandocService(pandocPath);
|
|
17
|
+
this.preprocessor = new MarkdownPreprocessor();
|
|
18
|
+
this.validator = new ContentValidator();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Initialize and check dependencies
|
|
22
|
+
*/
|
|
23
|
+
async initialize() {
|
|
24
|
+
const pandocInfo = await this.pandocService.checkPandocAvailable();
|
|
25
|
+
if (!pandocInfo.available) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: pandocInfo.error || 'Pandoc을 찾을 수 없습니다.',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
Logger.info('Initialized successfully', {
|
|
32
|
+
pandocVersion: pandocInfo.version,
|
|
33
|
+
pandocPath: pandocInfo.path,
|
|
34
|
+
});
|
|
35
|
+
return { success: true };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convert markdown to EPUB/PDF
|
|
39
|
+
*/
|
|
40
|
+
async convert(options) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const warnings = [];
|
|
43
|
+
try {
|
|
44
|
+
// Validate input
|
|
45
|
+
if (!fs.existsSync(options.inputPath)) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
errors: [`입력 파일을 찾을 수 없습니다: ${options.inputPath}`],
|
|
49
|
+
warnings: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Read markdown content
|
|
53
|
+
let content = fs.readFileSync(options.inputPath, 'utf-8');
|
|
54
|
+
// Step 1: Validate content
|
|
55
|
+
let validationReport;
|
|
56
|
+
if (options.validateContent !== false) {
|
|
57
|
+
validationReport = await this.validator.validate(content, options.inputPath);
|
|
58
|
+
if (validationReport.errors > 0) {
|
|
59
|
+
errors.push(`검증 오류: ${validationReport.errors}개 발견`);
|
|
60
|
+
}
|
|
61
|
+
if (validationReport.warnings > 0) {
|
|
62
|
+
warnings.push(`검증 경고: ${validationReport.warnings}개 발견`);
|
|
63
|
+
}
|
|
64
|
+
// Auto-fix if enabled
|
|
65
|
+
if (options.autoFix !== false && validationReport.fixedIssues > 0) {
|
|
66
|
+
content = this.validator.autoFix(content, validationReport);
|
|
67
|
+
warnings.push(`${validationReport.fixedIssues}개 문제 자동 수정됨`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Step 2: Preprocess markdown
|
|
71
|
+
const preprocessResult = await this.preprocessor.preprocess(content, options.inputPath, options.format === 'pdf' ? 'pdf' : 'epub');
|
|
72
|
+
warnings.push(...preprocessResult.warnings);
|
|
73
|
+
// Step 3: Generate clean markdown with frontmatter
|
|
74
|
+
const cleanMarkdown = this.preprocessor.generateCleanMarkdown(preprocessResult, options.format === 'pdf' ? 'pdf' : 'epub');
|
|
75
|
+
// Step 4: Write temporary markdown file
|
|
76
|
+
const tempDir = getTempDir();
|
|
77
|
+
const tempMarkdownPath = path.join(tempDir, 'temp.md');
|
|
78
|
+
fs.writeFileSync(tempMarkdownPath, cleanMarkdown, 'utf-8');
|
|
79
|
+
// Step 5: Determine output paths
|
|
80
|
+
const baseName = sanitizeFilename(preprocessResult.metadata.title || 'output');
|
|
81
|
+
const outputDir = options.outputPath || path.dirname(options.inputPath);
|
|
82
|
+
ensureDirectory(outputDir);
|
|
83
|
+
const epubPath = path.join(outputDir, `${baseName}.epub`);
|
|
84
|
+
const pdfPath = path.join(outputDir, `${baseName}.pdf`);
|
|
85
|
+
// Step 6: Convert to EPUB
|
|
86
|
+
if (options.format === 'epub' || options.format === 'both') {
|
|
87
|
+
const epubResult = await this.pandocService.toEpub({
|
|
88
|
+
inputPath: tempMarkdownPath,
|
|
89
|
+
outputPath: epubPath,
|
|
90
|
+
title: preprocessResult.metadata.title || 'Untitled',
|
|
91
|
+
author: preprocessResult.metadata.author,
|
|
92
|
+
language: preprocessResult.metadata.language,
|
|
93
|
+
coverImagePath: options.coverTheme ? undefined : undefined, // TODO: implement cover generation
|
|
94
|
+
cssPath: options.cssPath,
|
|
95
|
+
typographyPreset: options.typographyPreset,
|
|
96
|
+
tocDepth: options.tocDepth,
|
|
97
|
+
enableFontSubsetting: options.enableFontSubsetting,
|
|
98
|
+
content: cleanMarkdown,
|
|
99
|
+
});
|
|
100
|
+
if (!epubResult.success) {
|
|
101
|
+
errors.push(`EPUB 변환 실패: ${epubResult.error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Step 7: Convert to PDF
|
|
105
|
+
if (options.format === 'pdf' || options.format === 'both') {
|
|
106
|
+
const pdfResult = await this.pandocService.toPdf({
|
|
107
|
+
inputPath: tempMarkdownPath,
|
|
108
|
+
outputPath: pdfPath,
|
|
109
|
+
title: preprocessResult.metadata.title || 'Untitled',
|
|
110
|
+
author: preprocessResult.metadata.author,
|
|
111
|
+
language: preprocessResult.metadata.language,
|
|
112
|
+
cssPath: options.cssPath,
|
|
113
|
+
typographyPreset: options.typographyPreset,
|
|
114
|
+
pdfEngine: options.pdfEngine,
|
|
115
|
+
paperSize: options.paperSize,
|
|
116
|
+
tocDepth: options.tocDepth,
|
|
117
|
+
includeToc: options.includeToc,
|
|
118
|
+
enableFontSubsetting: options.enableFontSubsetting,
|
|
119
|
+
content: cleanMarkdown,
|
|
120
|
+
});
|
|
121
|
+
if (!pdfResult.success) {
|
|
122
|
+
errors.push(`PDF 변환 실패: ${pdfResult.error}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Clean up temp file
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(tempMarkdownPath);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore cleanup errors
|
|
131
|
+
}
|
|
132
|
+
// Return result
|
|
133
|
+
const success = errors.length === 0;
|
|
134
|
+
return {
|
|
135
|
+
success,
|
|
136
|
+
epubPath: options.format === 'epub' || options.format === 'both' ? epubPath : undefined,
|
|
137
|
+
pdfPath: options.format === 'pdf' || options.format === 'both' ? pdfPath : undefined,
|
|
138
|
+
errors,
|
|
139
|
+
warnings,
|
|
140
|
+
validationReport,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
147
|
+
warnings,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get Pandoc installation instructions
|
|
153
|
+
*/
|
|
154
|
+
static getInstallInstructions() {
|
|
155
|
+
return PandocService.getInstallInstructions();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Export types
|
|
159
|
+
export * from './types/index.js';
|
|
160
|
+
export * from './types/validators.js';
|
|
161
|
+
export { PandocService } from './services/PandocService.js';
|
|
162
|
+
export { MarkdownPreprocessor } from './services/MarkdownPreprocessor.js';
|
|
163
|
+
export { ContentValidator } from './services/ContentValidator.js';
|
|
164
|
+
export { DEFAULT_CONFIG } from './utils/constants.js';
|
|
165
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Validator - 8 Validation Modules
|
|
3
|
+
*/
|
|
4
|
+
import type { ValidationReport } from '../types/index.js';
|
|
5
|
+
import type { ValidatorConfig } from '../types/validators.js';
|
|
6
|
+
export declare class ContentValidator {
|
|
7
|
+
private config;
|
|
8
|
+
constructor(config?: Partial<ValidatorConfig>);
|
|
9
|
+
/**
|
|
10
|
+
* Run all validation modules
|
|
11
|
+
*/
|
|
12
|
+
validate(content: string, filePath?: string): Promise<ValidationReport>;
|
|
13
|
+
/**
|
|
14
|
+
* Module 1: Frontmatter validation
|
|
15
|
+
*/
|
|
16
|
+
private validateFrontmatter;
|
|
17
|
+
/**
|
|
18
|
+
* Module 2: Heading validation
|
|
19
|
+
*/
|
|
20
|
+
private validateHeadings;
|
|
21
|
+
/**
|
|
22
|
+
* Module 3: Link validation
|
|
23
|
+
*/
|
|
24
|
+
private validateLinks;
|
|
25
|
+
/**
|
|
26
|
+
* Module 4: Image validation
|
|
27
|
+
*/
|
|
28
|
+
private validateImages;
|
|
29
|
+
/**
|
|
30
|
+
* Module 5: Table validation
|
|
31
|
+
*/
|
|
32
|
+
private validateTables;
|
|
33
|
+
/**
|
|
34
|
+
* Module 6: Syntax validation
|
|
35
|
+
*/
|
|
36
|
+
private validateSyntax;
|
|
37
|
+
/**
|
|
38
|
+
* Module 7: Special character validation
|
|
39
|
+
*/
|
|
40
|
+
private validateSpecialCharacters;
|
|
41
|
+
/**
|
|
42
|
+
* Module 8: Accessibility validation
|
|
43
|
+
*/
|
|
44
|
+
private validateAccessibility;
|
|
45
|
+
/**
|
|
46
|
+
* Auto-fix content based on validation results
|
|
47
|
+
*/
|
|
48
|
+
autoFix(content: string, report: ValidationReport): string;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=ContentValidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContentValidator.d.ts","sourceRoot":"","sources":["../../src/services/ContentValidator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAoB,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGhF,qBAAa,gBAAgB;IACzB,OAAO,CAAC,MAAM,CAAkB;gBAEpB,MAAM,GAAE,OAAO,CAAC,eAAe,CAAM;IAWjD;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAkE7E;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuC3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA8CxB;;OAEG;IACH,OAAO,CAAC,aAAa;IAiDrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAoCtB;;OAEG;IACH,OAAO,CAAC,cAAc;IAoCtB;;OAEG;IACH,OAAO,CAAC,cAAc;IAiCtB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA8BjC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0C7B;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,MAAM;CA4B7D"}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Validator - 8 Validation Modules
|
|
3
|
+
*/
|
|
4
|
+
import { Logger } from '../utils/common.js';
|
|
5
|
+
export class ContentValidator {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.config = {
|
|
9
|
+
autoFix: config.autoFix ?? true,
|
|
10
|
+
strictMode: config.strictMode ?? false,
|
|
11
|
+
allowedImageFormats: config.allowedImageFormats ?? ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'],
|
|
12
|
+
maxImageSize: config.maxImageSize ?? 10 * 1024 * 1024, // 10MB
|
|
13
|
+
requireAltText: config.requireAltText ?? true,
|
|
14
|
+
checkExternalLinks: config.checkExternalLinks ?? false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run all validation modules
|
|
19
|
+
*/
|
|
20
|
+
async validate(content, filePath) {
|
|
21
|
+
const issues = [];
|
|
22
|
+
Logger.info('Starting content validation...');
|
|
23
|
+
// Module 1: Frontmatter validation
|
|
24
|
+
const frontmatterResult = this.validateFrontmatter(content);
|
|
25
|
+
issues.push(...frontmatterResult);
|
|
26
|
+
// Module 2: Heading validation
|
|
27
|
+
const headingResult = this.validateHeadings(content);
|
|
28
|
+
issues.push(...headingResult);
|
|
29
|
+
// Module 3: Link validation
|
|
30
|
+
const linkResult = this.validateLinks(content);
|
|
31
|
+
issues.push(...linkResult);
|
|
32
|
+
// Module 4: Image validation
|
|
33
|
+
const imageResult = this.validateImages(content);
|
|
34
|
+
issues.push(...imageResult);
|
|
35
|
+
// Module 5: Table validation
|
|
36
|
+
const tableResult = this.validateTables(content);
|
|
37
|
+
issues.push(...tableResult);
|
|
38
|
+
// Module 6: Syntax validation
|
|
39
|
+
const syntaxResult = this.validateSyntax(content);
|
|
40
|
+
issues.push(...syntaxResult);
|
|
41
|
+
// Module 7: Special character validation
|
|
42
|
+
const specialResult = this.validateSpecialCharacters(content);
|
|
43
|
+
issues.push(...specialResult);
|
|
44
|
+
// Module 8: Accessibility validation
|
|
45
|
+
const accessibilityResult = this.validateAccessibility(content);
|
|
46
|
+
issues.push(...accessibilityResult);
|
|
47
|
+
// Auto-fix if enabled
|
|
48
|
+
let fixedIssues = 0;
|
|
49
|
+
if (this.config.autoFix) {
|
|
50
|
+
for (const issue of issues) {
|
|
51
|
+
if (issue.fixed) {
|
|
52
|
+
fixedIssues++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
57
|
+
const warnings = issues.filter(i => i.type === 'warning');
|
|
58
|
+
Logger.info('Validation completed', {
|
|
59
|
+
total: issues.length,
|
|
60
|
+
errors: errors.length,
|
|
61
|
+
warnings: warnings.length,
|
|
62
|
+
fixed: fixedIssues,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
totalIssues: issues.length,
|
|
66
|
+
fixedIssues,
|
|
67
|
+
warnings: warnings.length,
|
|
68
|
+
errors: errors.length,
|
|
69
|
+
details: issues,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Module 1: Frontmatter validation
|
|
74
|
+
*/
|
|
75
|
+
validateFrontmatter(content) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
78
|
+
const match = content.match(frontmatterRegex);
|
|
79
|
+
if (!match) {
|
|
80
|
+
issues.push({
|
|
81
|
+
type: 'warning',
|
|
82
|
+
category: 'frontmatter',
|
|
83
|
+
message: 'YAML frontmatter가 없습니다. 메타데이터를 추가하는 것을 권장합니다.',
|
|
84
|
+
suggestion: '문서 상단에 ---로 감싸진 YAML frontmatter를 추가하세요.',
|
|
85
|
+
});
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
const yamlContent = match[1];
|
|
89
|
+
const lines = yamlContent.split('\n');
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
const line = lines[i];
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
94
|
+
continue;
|
|
95
|
+
const colonIndex = trimmed.indexOf(':');
|
|
96
|
+
if (colonIndex === -1) {
|
|
97
|
+
issues.push({
|
|
98
|
+
type: 'warning',
|
|
99
|
+
category: 'frontmatter',
|
|
100
|
+
message: `YAML 구문 오류: 콜론(:)이 누락됨`,
|
|
101
|
+
line: i + 1,
|
|
102
|
+
suggestion: 'key: value 형식을 확인하세요.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return issues;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Module 2: Heading validation
|
|
110
|
+
*/
|
|
111
|
+
validateHeadings(content) {
|
|
112
|
+
const issues = [];
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
const h1Titles = [];
|
|
115
|
+
let lastLevel = 0;
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
119
|
+
if (headingMatch) {
|
|
120
|
+
const level = headingMatch[1].length;
|
|
121
|
+
const title = headingMatch[2].trim();
|
|
122
|
+
// Check for duplicate H1
|
|
123
|
+
if (level === 1) {
|
|
124
|
+
if (h1Titles.includes(title)) {
|
|
125
|
+
issues.push({
|
|
126
|
+
type: 'warning',
|
|
127
|
+
category: 'heading',
|
|
128
|
+
message: `중복된 H1 제목: "${title}"`,
|
|
129
|
+
line: i + 1,
|
|
130
|
+
suggestion: 'H1은 문서당 하나만 사용하는 것을 권장합니다.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
h1Titles.push(title);
|
|
134
|
+
}
|
|
135
|
+
// Check for heading level gaps
|
|
136
|
+
if (lastLevel > 0 && level > lastLevel + 1) {
|
|
137
|
+
issues.push({
|
|
138
|
+
type: 'warning',
|
|
139
|
+
category: 'heading',
|
|
140
|
+
message: `제목 레벨 갭: H${lastLevel} → H${level}`,
|
|
141
|
+
line: i + 1,
|
|
142
|
+
suggestion: `H${lastLevel + 1}을 건너뛰었습니다.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
lastLevel = level;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Module 3: Link validation
|
|
152
|
+
*/
|
|
153
|
+
validateLinks(content) {
|
|
154
|
+
const issues = [];
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
// Check for Obsidian links [[link]]
|
|
157
|
+
const obsidianLinkRegex = /\[\[([^\]]+)\]\]/g;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = obsidianLinkRegex.exec(content)) !== null) {
|
|
160
|
+
issues.push({
|
|
161
|
+
type: 'info',
|
|
162
|
+
category: 'link',
|
|
163
|
+
message: `Obsidian 링크 발견: "${match[1]}"`,
|
|
164
|
+
suggestion: '자동으로 표준 마크다운 링크로 변환됩니다.',
|
|
165
|
+
fixed: true,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Check for broken markdown links
|
|
169
|
+
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
170
|
+
let linkMatch;
|
|
171
|
+
while ((linkMatch = markdownLinkRegex.exec(content)) !== null) {
|
|
172
|
+
const url = linkMatch[2];
|
|
173
|
+
// Check for empty URLs
|
|
174
|
+
if (!url || url.trim() === '') {
|
|
175
|
+
issues.push({
|
|
176
|
+
type: 'error',
|
|
177
|
+
category: 'link',
|
|
178
|
+
message: '빈 링크 URL',
|
|
179
|
+
suggestion: '링크 URL을 입력하세요.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Check for external links if enabled
|
|
183
|
+
if (this.config.checkExternalLinks && url.startsWith('http')) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: 'info',
|
|
186
|
+
category: 'link',
|
|
187
|
+
message: `외부 링크: ${url}`,
|
|
188
|
+
suggestion: '외부 링크는 변환 후에도 작동해야 합니다.',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return issues;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Module 4: Image validation
|
|
196
|
+
*/
|
|
197
|
+
validateImages(content) {
|
|
198
|
+
const issues = [];
|
|
199
|
+
// Check for images without alt text
|
|
200
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
201
|
+
let match;
|
|
202
|
+
while ((match = imageRegex.exec(content)) !== null) {
|
|
203
|
+
const alt = match[1];
|
|
204
|
+
const url = match[2];
|
|
205
|
+
// Check for missing alt text
|
|
206
|
+
if (this.config.requireAltText && (!alt || alt.trim() === '')) {
|
|
207
|
+
issues.push({
|
|
208
|
+
type: 'warning',
|
|
209
|
+
category: 'image',
|
|
210
|
+
message: '이미지 alt 텍스트가 없습니다.',
|
|
211
|
+
suggestion: '접근성을 위해 이미지에 설명을 추가하세요.',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Check image format
|
|
215
|
+
const ext = url.split('.').pop()?.toLowerCase();
|
|
216
|
+
if (ext && !this.config.allowedImageFormats.includes(ext)) {
|
|
217
|
+
issues.push({
|
|
218
|
+
type: 'warning',
|
|
219
|
+
category: 'image',
|
|
220
|
+
message: `지원하지 않는 이미지 형식: ${ext}`,
|
|
221
|
+
suggestion: `지원 형식: ${this.config.allowedImageFormats.join(', ')}`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return issues;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Module 5: Table validation
|
|
229
|
+
*/
|
|
230
|
+
validateTables(content) {
|
|
231
|
+
const issues = [];
|
|
232
|
+
const lines = content.split('\n');
|
|
233
|
+
let inTable = false;
|
|
234
|
+
let columnCount = 0;
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
const line = lines[i].trim();
|
|
237
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
238
|
+
if (!inTable) {
|
|
239
|
+
inTable = true;
|
|
240
|
+
const cells = line.split('|').filter(c => c.trim() !== '');
|
|
241
|
+
columnCount = cells.length;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const cells = line.split('|').filter(c => c.trim() !== '');
|
|
245
|
+
if (cells.length !== columnCount && !line.includes('---')) {
|
|
246
|
+
issues.push({
|
|
247
|
+
type: 'warning',
|
|
248
|
+
category: 'table',
|
|
249
|
+
message: `테이블 열 불일치: ${columnCount}개 예상, ${cells.length}개 발견`,
|
|
250
|
+
line: i + 1,
|
|
251
|
+
suggestion: '모든 행의 열 수를 일치시키세요.',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
inTable = false;
|
|
258
|
+
columnCount = 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return issues;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Module 6: Syntax validation
|
|
265
|
+
*/
|
|
266
|
+
validateSyntax(content) {
|
|
267
|
+
const issues = [];
|
|
268
|
+
// Check for unclosed code blocks
|
|
269
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
270
|
+
const codeBlockMatches = content.match(/```/g);
|
|
271
|
+
if (codeBlockMatches && codeBlockMatches.length % 2 !== 0) {
|
|
272
|
+
issues.push({
|
|
273
|
+
type: 'error',
|
|
274
|
+
category: 'syntax',
|
|
275
|
+
message: '닫히지 않은 코드 블록',
|
|
276
|
+
suggestion: '코드 블록을 ```로 닫으세요.',
|
|
277
|
+
fixed: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Check for unclosed inline code
|
|
281
|
+
const inlineCodeMatches = content.match(/`[^`]+`/g);
|
|
282
|
+
const backtickCount = (content.match(/`/g) || []).length;
|
|
283
|
+
if (backtickCount % 2 !== 0) {
|
|
284
|
+
issues.push({
|
|
285
|
+
type: 'warning',
|
|
286
|
+
category: 'syntax',
|
|
287
|
+
message: '닫히지 않은 인라인 코드',
|
|
288
|
+
suggestion: '인라인 코드를 `로 닫으세요.',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return issues;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Module 7: Special character validation
|
|
295
|
+
*/
|
|
296
|
+
validateSpecialCharacters(content) {
|
|
297
|
+
const issues = [];
|
|
298
|
+
// Check for emojis (may not render correctly in some formats)
|
|
299
|
+
const emojiRegex = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
|
300
|
+
const emojis = content.match(emojiRegex);
|
|
301
|
+
if (emojis && emojis.length > 0) {
|
|
302
|
+
issues.push({
|
|
303
|
+
type: 'warning',
|
|
304
|
+
category: 'syntax',
|
|
305
|
+
message: `${emojis.length}개의 이모지 발견`,
|
|
306
|
+
suggestion: '일부 형식에서 이모지가 올바르게 렌더링되지 않을 수 있습니다.',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// Check for ASCII diagrams
|
|
310
|
+
const asciiDiagramRegex = /[+\-|\/\\]{10,}/g;
|
|
311
|
+
if (asciiDiagramRegex.test(content)) {
|
|
312
|
+
issues.push({
|
|
313
|
+
type: 'info',
|
|
314
|
+
category: 'syntax',
|
|
315
|
+
message: 'ASCII 다이어그램 발견',
|
|
316
|
+
suggestion: '다이어그램은 이미지로 변환하는 것을 권장합니다.',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return issues;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Module 8: Accessibility validation
|
|
323
|
+
*/
|
|
324
|
+
validateAccessibility(content) {
|
|
325
|
+
const issues = [];
|
|
326
|
+
// Check for proper heading hierarchy
|
|
327
|
+
const lines = content.split('\n');
|
|
328
|
+
let hasH1 = false;
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
if (line.startsWith('# ')) {
|
|
331
|
+
hasH1 = true;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!hasH1) {
|
|
336
|
+
issues.push({
|
|
337
|
+
type: 'warning',
|
|
338
|
+
category: 'accessibility',
|
|
339
|
+
message: 'H1 제목이 없습니다.',
|
|
340
|
+
suggestion: '문서에 H1 제목을 추가하세요.',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// Check for long paragraphs (hard to read)
|
|
344
|
+
const paragraphs = content.split('\n\n');
|
|
345
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
346
|
+
const paragraph = paragraphs[i].trim();
|
|
347
|
+
const wordCount = paragraph.split(/\s+/).length;
|
|
348
|
+
if (wordCount > 300) {
|
|
349
|
+
issues.push({
|
|
350
|
+
type: 'info',
|
|
351
|
+
category: 'accessibility',
|
|
352
|
+
message: `긴 문단 발견 (${wordCount}단어)`,
|
|
353
|
+
suggestion: '가독성을 위해 문단을 나누는 것을 권장합니다.',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return issues;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Auto-fix content based on validation results
|
|
361
|
+
*/
|
|
362
|
+
autoFix(content, report) {
|
|
363
|
+
let fixedContent = content;
|
|
364
|
+
for (const issue of report.details) {
|
|
365
|
+
if (issue.fixed && issue.category === 'link') {
|
|
366
|
+
// Fix Obsidian links
|
|
367
|
+
fixedContent = fixedContent.replace(/\[\[([^\]]+)\]\]/g, (match, link) => {
|
|
368
|
+
const parts = link.split('|');
|
|
369
|
+
const text = parts[1] || parts[0];
|
|
370
|
+
const target = parts[0];
|
|
371
|
+
return target.startsWith('http') ? `[${text}](${target})` : `[${text}](${target}.md)`;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (issue.fixed && issue.category === 'syntax') {
|
|
375
|
+
// Fix unclosed code blocks
|
|
376
|
+
const codeBlockCount = (fixedContent.match(/```/g) || []).length;
|
|
377
|
+
if (codeBlockCount % 2 !== 0) {
|
|
378
|
+
fixedContent += '\n```';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return fixedContent;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
//# sourceMappingURL=ContentValidator.js.map
|