Spaces:
Running
Running
Download data-processor.js from smmisha/omniconvert: direct link, hf CLI and curl.
- Browser
- Download file 29.7 kB
-
https://huggingface.co/spaces/smmisha/omniconvert/resolve/main/data-processor.js
- Command line
-
hf download hf://spaces/smmisha/omniconvert/data-processor.js
-
curl -L -o data-processor.js https://huggingface.co/spaces/smmisha/omniconvert/resolve/main/data-processor.js
29.7 kB
| // ========================================== | |
| // 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('<?xml') || str.startsWith('<')) { | |
| // Check if it looks more like HTML code or XML document | |
| if (str.toLowerCase().includes('<!doctype html>') || str.toLowerCase().includes('<html')) { | |
| return 'html'; | |
| } | |
| return 'xml'; | |
| } | |
| if (str.includes('[') && str.includes(']') && str.includes('=')) return 'ini'; | |
| if (str.includes('|') && str.includes('---')) return 'md_table'; | |
| if (str.includes('\n') && (str.includes(',') || str.includes(';'))) { | |
| return 'csv'; | |
| } | |
| // Check if it has markdown formatting | |
| if (str.includes('# ') || str.includes('**') || str.includes('* ')) { | |
| return 'md_text'; | |
| } | |
| if (str.includes('--- Страница ')) { | |
| return 'pdf'; | |
| } | |
| return 'yaml'; | |
| }, | |
| // Parsing Router | |
| parseData(str, format) { | |
| switch (format) { | |
| case 'json': | |
| return JSON.parse(str); | |
| case 'csv': | |
| return this.parseCSV(str); | |
| case 'xml': | |
| return this.parseXML(str); | |
| case 'yaml': | |
| return this.parseYAML(str); | |
| case 'ini': | |
| return this.parseINI(str); | |
| case 'md_table': | |
| return this.parseMarkdownTable(str); | |
| case 'md_text': | |
| case 'html': | |
| case 'pdf': | |
| return str; // Plain string representations for HTML / Markdown / PDF texts | |
| default: | |
| throw new Error(`Неподдерживаемый формат разбора: ${format}`); | |
| } | |
| }, | |
| // Serializing Router | |
| stringifyData(obj, format) { | |
| switch (format) { | |
| case 'json': | |
| // If obj is a plain text from HTML/Markdown parser, wrap it in JSON string | |
| if (typeof obj === 'string') return JSON.stringify({ text: obj }, null, 2); | |
| return JSON.stringify(obj, null, 2); | |
| case 'csv': | |
| if (typeof obj === 'string') throw new Error('Невозможно преобразовать текстовый документ в плоскую CSV таблицу'); | |
| return this.stringifyCSV(obj); | |
| case 'xml': | |
| if (typeof obj === 'string') return `<?xml version="1.0" encoding="UTF-8"?>\n<text>${this.escapeXML(obj)}</text>`; | |
| 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('</')) { | |
| return this.convertHtmlToMarkdown(obj); | |
| } | |
| return obj; | |
| } | |
| return '```yaml\n' + this.stringifyYAML(obj) + '\n```'; | |
| case 'html': | |
| if (typeof obj === 'string') { | |
| // Convert MD to HTML | |
| return this.convertMarkdownToHtml(obj); | |
| } | |
| return '<pre><code>' + JSON.stringify(obj, null, 2) + '</code></pre>'; | |
| 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))}</${nodeName}>`; | |
| } | |
| 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}</${nodeName}>`; | |
| } else if (val['#text']) { | |
| return `${indent}<${nodeName}${attributesStr}>${this.escapeXML(String(val['#text']))}</${nodeName}>`; | |
| } else { | |
| return `${indent}<${nodeName}${attributesStr} />`; | |
| } | |
| }; | |
| return `<?xml version="1.0" encoding="UTF-8"?>\n${objectToXmlStr(rootName, contentObj)}`; | |
| }, | |
| escapeXML(str) { | |
| return str | |
| .replace(/&/g, '&') | |
| .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+</g, '><'); | |
| } | |
| 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(); | |
| }); | |