// ========================================== // OmniConvert: Data Converter Module // ========================================== const DataHub = { // Bind UI Controls init() { this.bindEvents(); }, bindEvents() { const $ = (id) => document.getElementById(id); // Text area counters $('dataInputTextArea').addEventListener('input', () => this.updateCounters()); // Paste button $('pasteInputBtn').addEventListener('click', async () => { try { const text = await navigator.clipboard.readText(); $('dataInputTextArea').value = text; this.updateCounters(); App.showToast('Текст вставлен из буфера', 'success'); } catch (err) { App.showToast('Не удалось получить доступ к буферу', 'error'); } }); // File loader trigger $('loadInputFileBtn').addEventListener('click', () => { $('dataFileLoader').click(); }); // File loader event $('dataFileLoader').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const ext = file.name.split('.').pop().toLowerCase(); if (ext === 'pdf') { App.showToast(`Чтение PDF "${file.name}"...`, 'info'); try { const extractedText = await this.extractTextFromPDF(file); $('dataInputTextArea').value = extractedText; this.updateCounters(); this.autoDetectFormat(file.name); App.showToast(`Текст из PDF "${file.name}" успешно извлечен`, 'success'); } catch (err) { console.error(err); App.showToast(`Ошибка чтения PDF: ${err.message}`, 'error'); } } else { const reader = new FileReader(); reader.onload = (evt) => { $('dataInputTextArea').value = evt.target.result; this.updateCounters(); this.autoDetectFormat(file.name); App.showToast(`Файл "${file.name}" загружен`, 'success'); }; reader.onerror = () => { App.showToast('Ошибка при чтении текстового файла', 'error'); }; reader.readAsText(file); } }); // Copy output $('copyOutputBtn').addEventListener('click', async () => { const outText = $('dataOutputTextArea').value; if (!outText) { App.showToast('Нет данных для копирования', 'warning'); return; } try { await navigator.clipboard.writeText(outText); App.showToast('Сконвертированный текст скопирован!', 'success'); } catch (e) { App.showToast('Ошибка копирования', 'error'); } }); // Download output file $('downloadOutputBtn').addEventListener('click', async () => { const outText = document.getElementById('dataOutputTextArea').value; if (!outText) { App.showToast('Нет данных для скачивания', 'warning'); return; } const fmt = document.getElementById('targetFormatSelect').value; const ext = this.getFormatExtension(fmt); let blob; if (fmt === 'pdf') { App.showToast('Генерация PDF документа...', 'info'); try { blob = await this.textToPDF(outText); } catch (e) { App.showToast('Ошибка создания PDF: ' + e.message, 'error'); return; } } else { blob = new Blob([outText], { type: 'text/plain;charset=utf-8' }); } const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `omniconvert_export_${Date.now()}.${ext}`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 4000); App.showToast('Файл скачан', 'success'); }); // Format & Minify utilities $('formatOutputBtn').addEventListener('click', () => this.beautifyOutput()); $('minifyOutputBtn').addEventListener('click', () => this.minifyOutput()); // Core conversion trigger button $('convertDataBtn').addEventListener('click', () => this.convert()); }, // Line count counters update updateCounters() { const input = document.getElementById('dataInputTextArea').value; const output = document.getElementById('dataOutputTextArea').value; const countLines = (str) => str ? str.split('\n').length : 0; const label = I18n ? I18n.t('lines_count', 'Строк:') : 'Строк:'; document.getElementById('inputLinesCount').textContent = `${label} ${countLines(input)}`; document.getElementById('outputLinesCount').textContent = `${label} ${countLines(output)}`; }, // Detect format based on file extension autoDetectFormat(filename) { const ext = filename.split('.').pop().toLowerCase(); const select = document.getElementById('sourceFormatSelect'); if (ext === 'json') select.value = 'json'; else if (ext === 'csv') select.value = 'csv'; else if (ext === 'xml') select.value = 'xml'; else if (['yaml', 'yml'].includes(ext)) select.value = 'yaml'; else if (ext === 'ini') select.value = 'ini'; else if (ext === 'md') select.value = 'md_text'; else if (ext === 'html') select.value = 'html'; else if (ext === 'pdf') select.value = 'pdf'; else select.value = 'auto'; }, // Main converter coordinator convert() { const inputTextArea = document.getElementById('dataInputTextArea'); const outputTextArea = document.getElementById('dataOutputTextArea'); const inputData = inputTextArea.value.trim(); if (!inputData) { App.showToast('Пожалуйста, введите исходные данные', 'warning'); return; } let srcFmt = document.getElementById('sourceFormatSelect').value; const dstFmt = document.getElementById('targetFormatSelect').value; if (srcFmt === 'auto') { srcFmt = this.detectFormatFromContent(inputData); App.showToast(`Автоопределение формата: ${srcFmt.toUpperCase()}`, 'info'); } try { // 1. Parse Source Format -> Intermediate JS Object const parsedObject = this.parseData(inputData, srcFmt); // 2. Stringify Intermediate JS Object -> Destination Format const outputText = this.stringifyData(parsedObject, dstFmt); outputTextArea.value = outputText; this.updateCounters(); App.showToast('Конвертация завершена успешно!', 'success'); } catch (err) { console.error(err); outputTextArea.value = `[ОШИБКА КОНВЕРТАЦИИ]:\n${err.message}`; this.updateCounters(); App.showToast('Ошибка конвертации. Подробности в окне вывода.', 'error'); } }, // Heuristic content-based format checker detectFormatFromContent(str) { if (str.startsWith('{') || str.startsWith('[')) return 'json'; if (str.startsWith('') || str.toLowerCase().includes('\n${this.escapeXML(obj)}`; return this.stringifyXML(obj); case 'yaml': if (typeof obj === 'string') return window.jsyaml ? window.jsyaml.dump({ text: obj }) : `text: ${obj}`; return this.stringifyYAML(obj); case 'ini': if (typeof obj === 'string') return `text = ${obj}`; return this.stringifyINI(obj); case 'md_table': if (typeof obj === 'string') throw new Error('Невозможно преобразовать текстовый документ в Markdown-таблицу'); return this.stringifyMarkdownTable(obj); case 'md_text': if (typeof obj === 'string') { // If it's HTML, convert to MD if (obj.trim().startsWith('<') || obj.includes('' + JSON.stringify(obj, null, 2) + ''; case 'pdf': if (typeof obj === 'string') { return obj; } try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); } default: throw new Error(`Неподдерживаемый формат сборки: ${format}`); } }, // ========================================== // PARSERS IMPLEMENTATION // ========================================== // CSV -> Array of Objects parser parseCSV(str) { const lines = str.split(/\r?\n/).filter(line => line.trim() !== ''); if (lines.length === 0) return []; // Auto-detect separator: comma or semicolon const headerLine = lines[0]; const separator = headerLine.includes(';') ? ';' : ','; const parseCSVLine = (line) => { const result = []; let cell = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === separator && !inQuotes) { result.push(cell.trim().replace(/^"|"$/g, '')); cell = ''; } else { cell += char; } } result.push(cell.trim().replace(/^"|"$/g, '')); return result; }; const headers = parseCSVLine(lines[0]); const items = []; for (let i = 1; i < lines.length; i++) { const cells = parseCSVLine(lines[i]); // Pad or trim columns to match headers size const item = {}; headers.forEach((h, index) => { item[h] = cells[index] !== undefined ? cells[index] : ''; }); items.push(item); } return items; }, // XML Parser converting nodes to JS structures parseXML(str) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(str, 'text/xml'); // Check for parser errors const parserErrorNode = xmlDoc.querySelector('parsererror'); if (parserErrorNode) { throw new Error(`XML Parser Error: ${parserErrorNode.textContent}`); } const xmlNodeToObject = (node) => { // If node is a text node if (node.nodeType === Node.TEXT_NODE) { return node.nodeValue; } const obj = {}; // Node attributes if (node.attributes && node.attributes.length > 0) { obj['@attributes'] = {}; for (let j = 0; j < node.attributes.length; j++) { const attribute = node.attributes.item(j); obj['@attributes'][attribute.nodeName] = attribute.nodeValue; } } // Child nodes if (node.childNodes && node.childNodes.length > 0) { for (let i = 0; i < node.childNodes.length; i++) { const item = node.childNodes.item(i); if (item.nodeType === Node.ELEMENT_NODE) { const nodeName = item.nodeName; const nodeVal = xmlNodeToObject(item); if (obj[nodeName] === undefined) { obj[nodeName] = nodeVal; } else { // Convert duplicate elements to array if (!Array.isArray(obj[nodeName])) { obj[nodeName] = [obj[nodeName]]; } obj[nodeName].push(nodeVal); } } else if (item.nodeType === Node.TEXT_NODE && item.nodeValue.trim() !== '') { if (node.childNodes.length === 1) { return item.nodeValue.trim(); } else { obj['#text'] = item.nodeValue.trim(); } } } } return Object.keys(obj).length === 0 ? "" : obj; }; const rootNode = xmlDoc.documentElement; const result = {}; result[rootNode.nodeName] = xmlNodeToObject(rootNode); return result; }, // YAML parser using local library parseYAML(str) { if (!window.jsyaml) { throw new Error('Модуль js-yaml не загружен. Проверьте lib/js-yaml.min.js'); } return window.jsyaml.load(str); }, // INI Parser parseINI(str) { const result = {}; let currentSection = result; const lines = str.split(/\r?\n/); lines.forEach(line => { line = line.trim(); if (!line || line.startsWith(';') || line.startsWith('#')) return; // comment / blank if (line.startsWith('[') && line.endsWith(']')) { const sectionName = line.slice(1, -1).trim(); result[sectionName] = {}; currentSection = result[sectionName]; } else { const parts = line.split('='); if (parts.length >= 2) { const key = parts[0].trim(); const val = parts.slice(1).join('=').trim(); currentSection[key] = val; } } }); return result; }, // ========================================== // SERIALIZERS IMPLEMENTATION // ========================================== // Convert array of objects to CSV sheet stringifyCSV(obj) { let items = Array.isArray(obj) ? obj : [obj]; // Resolve nested arrays if XML parsed directly if (items.length === 1 && typeof items[0] === 'object' && !Array.isArray(items[0])) { const keys = Object.keys(items[0]); if (keys.length === 1 && Array.isArray(items[0][keys[0]])) { items = items[0][keys[0]]; } } if (items.length === 0 || typeof items[0] !== 'object') { return 'Header\nEmpty dataset or invalid format'; } // Extract headers (flatten fields slightly if object) const headersSet = new Set(); items.forEach(item => { if (item && typeof item === 'object') { Object.keys(item).forEach(k => headersSet.add(k)); } }); const headers = Array.from(headersSet); const escapeCSV = (val) => { if (val === null || val === undefined) return ''; let str = typeof val === 'object' ? JSON.stringify(val) : String(val); str = str.replace(/"/g, '""'); if (str.includes(',') || str.includes(';') || str.includes('\n') || str.includes('"')) { return `"${str}"`; } return str; }; const separator = ','; const csvLines = [headers.join(separator)]; items.forEach(item => { const row = headers.map(h => escapeCSV(item[h])); csvLines.push(row.join(separator)); }); return csvLines.join('\n'); }, // Object to XML converter stringifyXML(obj) { let rootName = 'root'; let contentObj = obj; // XML needs exactly one root node if (obj && typeof obj === 'object' && !Array.isArray(obj)) { const keys = Object.keys(obj); if (keys.length === 1) { rootName = keys[0]; contentObj = obj[keys[0]]; } } const objectToXmlStr = (nodeName, val, indent = '') => { if (val === null || val === undefined) { return `${indent}<${nodeName} />`; } if (typeof val !== 'object') { return `${indent}<${nodeName}>${this.escapeXML(String(val))}`; } if (Array.isArray(val)) { return val.map(item => objectToXmlStr(nodeName, item, indent)).join('\n'); } let attributesStr = ''; if (val['@attributes']) { attributesStr = Object.keys(val['@attributes']) .map(k => ` ${k}="${this.escapeXML(String(val['@attributes'][k]))}"`) .join(''); } const childNodesStr = Object.keys(val) .filter(k => k !== '@attributes' && k !== '#text') .map(k => objectToXmlStr(k, val[k], indent + ' ')) .join('\n'); if (childNodesStr) { return `${indent}<${nodeName}${attributesStr}>\n${childNodesStr}\n${indent}`; } else if (val['#text']) { return `${indent}<${nodeName}${attributesStr}>${this.escapeXML(String(val['#text']))}`; } else { return `${indent}<${nodeName}${attributesStr} />`; } }; return `\n${objectToXmlStr(rootName, contentObj)}`; }, escapeXML(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, // YAML Stringifier using local library stringifyYAML(obj) { if (!window.jsyaml) { throw new Error('Модуль js-yaml не загружен. Проверьте lib/js-yaml.min.js'); } return window.jsyaml.dump(obj, { indent: 2 }); }, // Object to INI serializer stringifyINI(obj) { if (typeof obj !== 'object' || Array.isArray(obj)) { throw new Error('INI формат поддерживает только ассоциативные словари (key-value)'); } const lines = []; const globalKeys = []; const sections = []; // Categorize top-level keys Object.keys(obj).forEach(k => { if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) { sections.push(k); } else { globalKeys.push(k); } }); // Write global keys globalKeys.forEach(k => { lines.push(`${k} = ${obj[k]}`); }); if (globalKeys.length > 0 && sections.length > 0) lines.push(''); // Write sections sections.forEach(secName => { lines.push(`[${secName}]`); const sectionObj = obj[secName]; Object.keys(sectionObj).forEach(k => { lines.push(`${k} = ${sectionObj[k]}`); }); lines.push(''); }); return lines.join('\n').trim(); }, // XML / JSON beautifiers beautifyOutput() { const area = document.getElementById('dataOutputTextArea'); const val = area.value.trim(); if (!val) return; const fmt = document.getElementById('targetFormatSelect').value; try { if (fmt === 'json') { const obj = JSON.parse(val); area.value = JSON.stringify(obj, null, 2); } else if (fmt === 'xml') { // XML is already formatted nicely in stringifyXML, but we can do a DOMParser format const parser = new DOMParser(); const xmlDoc = parser.parseFromString(val, 'application/xml'); const serializer = new XMLSerializer(); // Since we don't have standard browser formatters, simple XML formatting: area.value = this.formatXMLString(serializer.serializeToString(xmlDoc)); } this.updateCounters(); App.showToast('Текст отформатирован', 'success'); } catch (e) { App.showToast('Невозможно отформатировать: неверная структура', 'warning'); } }, minifyOutput() { const area = document.getElementById('dataOutputTextArea'); const val = area.value.trim(); if (!val) return; const fmt = document.getElementById('targetFormatSelect').value; try { if (fmt === 'json') { const obj = JSON.parse(val); area.value = JSON.stringify(obj); } else if (fmt === 'xml') { // strip spaces between tags area.value = val.replace(/>\s+<'); } this.updateCounters(); App.showToast('Текст минифицирован (сжат)', 'success'); } catch (e) { App.showToast('Невозможно минифицировать: неверная структура', 'warning'); } }, formatXMLString(xml) { let formatted = ''; let indent = ''; const reg = /(>)(<)(\/*)/g; xml = xml.replace(reg, '$1\r\n$2$3'); let pad = 0; xml.split('\r\n').forEach(node => { let indent = 0; if (node.match(/.+<\/\w[^>]*>$/)) { indent = 0; } else if (node.match(/^<\/\w/)) { if (pad !== 0) { pad -= 1; } } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) { indent = 1; } else { indent = 0; } let padding = ''; for (let i = 0; i < pad; i++) { padding += ' '; } formatted += padding + node + '\r\n'; pad += indent; }); return formatted.trim(); }, getFormatExtension(fmt) { const mapping = { json: 'json', csv: 'csv', xml: 'xml', yaml: 'yaml', ini: 'ini', md_table: 'md', md_text: 'md', html: 'html', pdf: 'pdf' }; return mapping[fmt] || 'txt'; }, // ========================================== // MARKDOWN & HTML PROCESSING IMPLEMENTATION // ========================================== // Parser: Markdown table -> JS Array of Objects parseMarkdownTable(str) { const lines = str.split(/\r?\n/).map(l => l.trim()).filter(l => l !== ''); if (lines.length === 0) return []; // Find lines with table pipes const rowLines = lines.filter(line => line.includes('|')); if (rowLines.length < 2) { throw new Error('Markdown-таблица должна содержать заголовок и разделитель (например: | header | |---|)'); } const parseRow = (line) => { let clean = line; if (clean.startsWith('|')) clean = clean.slice(1); if (clean.endsWith('|')) clean = clean.slice(0, -1); return clean.split('|').map(cell => cell.trim()); }; const headers = parseRow(rowLines[0]); const rows = []; // Line index 1 is separator (e.g. |---|---|) const separator = rowLines[1]; if (!separator.includes('-')) { throw new Error('Вторая строка Markdown-таблицы должна содержать разделители "---"'); } for (let i = 2; i < rowLines.length; i++) { const cells = parseRow(rowLines[i]); const rowObj = {}; headers.forEach((h, idx) => { rowObj[h] = cells[idx] !== undefined ? cells[idx] : ''; }); rows.push(rowObj); } return rows; }, // Serializer: JS Array of Objects -> Markdown table stringifyMarkdownTable(obj) { let items = Array.isArray(obj) ? obj : [obj]; // Resolve nested arrays from XML nodes if (items.length === 1 && typeof items[0] === 'object' && !Array.isArray(items[0])) { const keys = Object.keys(items[0]); if (keys.length === 1 && Array.isArray(items[0][keys[0]])) { items = items[0][keys[0]]; } } if (items.length === 0 || typeof items[0] !== 'object') { return '| Сообщение |\n| --- |\n| Пустой набор данных |'; } const headersSet = new Set(); items.forEach(item => { if (item && typeof item === 'object') { Object.keys(item).forEach(k => headersSet.add(k)); } }); const headers = Array.from(headersSet); if (headers.length === 0) { return '| Сообщение |\n| --- |\n| Пустой набор данных |'; } const headerRow = '| ' + headers.join(' | ') + ' |'; const separatorRow = '| ' + headers.map(() => '---').join(' | ') + ' |'; const valueRows = items.map(item => { const rowValues = headers.map(h => { const val = item[h]; if (val === null || val === undefined) return ''; return typeof val === 'object' ? JSON.stringify(val) : String(val); }); return '| ' + rowValues.join(' | ') + ' |'; }); return [headerRow, separatorRow, ...valueRows].join('\n'); }, // Offline Markdown to HTML conversion convertMarkdownToHtml(str) { if (typeof window.marked === 'undefined') { throw new Error('Библиотека marked.js не загружена. Проверьте lib/marked.min.js'); } // Support marked parsing APIs if (typeof window.marked.parse === 'function') { return window.marked.parse(str); } else if (typeof window.marked === 'function') { return window.marked(str); } throw new Error('Сбой инициализации парсера marked.js'); }, // Offline HTML to Markdown conversion convertHtmlToMarkdown(str) { if (typeof window.TurndownService === 'undefined') { throw new Error('Библиотека turndown.js не загружена. Проверьте lib/turndown.js'); } const turndownService = new window.TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', emDelimiter: '*' }); return turndownService.turndown(str); }, // Compile Russian and multilingual text into PDF natively offline async textToPDF(text) { if (typeof window.jspdf === 'undefined' || !window.jspdf.jsPDF) { throw new Error('Библиотека jsPDF не загружена. Проверьте lib/jspdf.umd.min.js'); } const lines = text.split('\n'); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const padding = 40; const fontSize = 14; const lineHeight = 22; const width = 800; // Calculate required height based on line numbers (min page format aspect ratio height) const height = Math.max(1130, padding * 2 + lines.length * lineHeight); canvas.width = width; canvas.height = height; // Fill background with white ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, height); // Draw text ctx.fillStyle = '#111827'; ctx.font = `${fontSize}px Arial, Helvetica, sans-serif`; let y = padding + fontSize; lines.forEach(line => { ctx.fillText(line, padding, y); y += lineHeight; }); const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'p', unit: 'px', format: [width, height] }); const imgData = canvas.toDataURL('image/jpeg', 0.95); doc.addImage(imgData, 'JPEG', 0, 0, width, height); return doc.output('blob'); }, // Extract text content from PDF pages natively offline async extractTextFromPDF(file) { const pdfjsLib = window.pdfjsLib || window['pdfjs-dist/build/pdf']; if (!pdfjsLib) { throw new Error('Библиотека PDF.js не загружена. Проверьте lib/pdf.min.js'); } pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js'; const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const numPages = pdf.numPages; App.showToast(`Извлечение текста из PDF: ${numPages} стр.`, 'info'); let fullText = ''; for (let pageNum = 1; pageNum <= numPages; pageNum++) { const page = await pdf.getPage(pageNum); const textContent = await page.getTextContent(); const textItems = textContent.items; let pageText = ''; let lastY = null; for (const item of textItems) { // If Y-coordinate changes by more than 10px, start a new line if (lastY !== null && Math.abs(item.transform[5] - lastY) > 10) { pageText += '\n'; } pageText += item.str; lastY = item.transform[5]; } if (pageText.trim()) { fullText += `--- Страница ${pageNum} ---\n${pageText.trim()}\n\n`; } } return fullText.trim(); } }; // Initialize module document.addEventListener('DOMContentLoaded', () => { DataHub.init(); });