UNPKG

@liascript/exporter

Version:
532 lines (477 loc) 14.7 kB
import * as helper from './helper' import * as RDF from './rdf' import * as COLOR from '../colorize' const path = require('path') const fs = require('fs-extra') const archiver = require('archiver') export function help() { console.log('') console.log(COLOR.heading('H5P settings:'), '\n') COLOR.info( 'H5P is a free and open-source content collaboration framework based on JavaScript that makes it easy to create, share and reuse interactive HTML5 content.' ) console.log('\nLearn more: https://h5p.org/\n') COLOR.command( null, '--h5p-embed', ' embed the Markdown into the JS code' ) COLOR.command( null, '--h5p-iframe', ' use an iframe for content display' ) COLOR.command( null, '--h5p-zip', ' package the output as a .h5p file' ) } export async function exporter( argument: { input: string readme: string output: string format: string path: string key?: string style?: string // special cases for H5P 'h5p-embed'?: string 'h5p-iframe'?: boolean 'h5p-zip'?: boolean }, json: any ) { // make temp folder let tmp = await helper.tmpDir() const dirname = helper.dirname() // Create a standard H5P package structure - this MUST be at the root level, not in subdirectories let h5pRoot = path.join(tmp, 'h5p_package') await fs.ensureDir(h5pRoot) // Extract the markdown content const markdownContent = argument['h5p-embed'] || (fs.existsSync(path.join(argument.path, argument.readme)) ? fs.readFileSync(path.join(argument.path, argument.readme), 'utf8') : '# LiaScript Content\n\nThis content was exported from LiaScript.') // Save the markdown content directly to the root with .md extension (allowed in whitelist) await helper.writeFile( path.join(h5pRoot, 'liascript_content.md'), markdownContent ) // Create h5p.json metadata file (required for all H5P packages) await helper.writeFile( path.join(h5pRoot, 'h5p.json'), JSON.stringify( { title: json.lia.str_title || 'LiaScript Course', language: json.lia.definition.language || 'en', mainLibrary: 'H5P.Text', embedTypes: ['iframe'], preloadedDependencies: [ { machineName: 'H5P.Text', majorVersion: 1, minorVersion: 1, }, ], author: json.lia.definition.author || '', license: 'CC BY-SA', licenseVersion: '4.0', description: json.lia.comment || '', contentType: 'Interactive Course', }, null, 2 ) ) // Parse the markdown content to create HTML const htmlContent = convertMarkdownToHTML(markdownContent) // First, copy the LiaScript assets to the temp directory let liaAssetPath = path.join(tmp, 'lia_assets') await fs.copy(path.join(dirname, './assets/h5p'), liaAssetPath) // await fs.copy(path.join(dirname, './assets/common'), liaAssetPath) // Generate the self-contained HTML const indexPath = path.join(liaAssetPath, 'index.html') const selfContainedHtml = await createSelfContainedHtml( indexPath, liaAssetPath, markdownContent // Pass the markdown content as a parameter ) // Create content.json with the self-contained HTML const contentJson = { text: selfContainedHtml, metadata: { contentType: 'Text', license: 'U', title: json.lia.str_title || 'LiaScript Course', authors: [ { name: json.lia.definition.author || 'LiaScript Author', role: 'Author', }, ], }, } // Save content.json directly to the root await helper.writeFile( path.join(h5pRoot, 'content.json'), JSON.stringify(contentJson, null, 2) ) // Now copy the H5P assets AFTER creating the basic structure try { // Copy the content files from the H5P templates - these should be pre-structured correctly await fs.copy(path.join(dirname, './assets/h5p'), h5pRoot, { overwrite: false, // Don't overwrite existing files (like content.json) filter: (src) => { const ext = path.extname(src).toLowerCase() // Only copy files with extensions, not directories alone if (fs.statSync(src).isDirectory()) { return true // Allow directories in copying } // Ensure the file has a valid extension from the whitelist return ext !== '' // Only copy files with extensions }, }) console.log('Copied H5P template/assets to package') } catch (error) { console.warn('Error copying H5P assets:', error) } // List of allowed extensions for H5P const allowedExtensions = [ 'json', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'eot', 'ttf', 'woff', 'woff2', 'otf', 'webm', 'mp4', 'ogg', 'mp3', 'm4a', 'wav', 'txt', 'pdf', 'rtf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'csv', 'diff', 'patch', 'swf', 'md', 'textile', 'vtt', 'webvtt', 'gltf', 'glb', 'js', 'css', 'svg', 'xml', ] // Copy user's content files with allowed extensions if (fs.existsSync(argument.path)) { try { // Get all files to copy - only files, not directories const getAllFiles = function ( dirPath: string, arrayOfFiles: { source: string; target: string }[] = [] ) { const files = fs.readdirSync(dirPath) files.forEach(function (file) { const filePath = path.join(dirPath, file) const stats = fs.statSync(filePath) if (stats.isDirectory()) { // Recurse into subdirectories arrayOfFiles = getAllFiles(filePath, arrayOfFiles) } else { const ext = path.extname(file).toLowerCase().substring(1) if (allowedExtensions.includes(ext)) { // Use flat structure - put all files at root with prefixed names to avoid collisions // This avoids directory whitelist issues const flatFileName = path .relative(argument.path, filePath) .replace(/\//g, '_') // Replace slashes with underscores .replace(/\\/g, '_') // Handle Windows paths too arrayOfFiles.push({ source: filePath, target: path.join(h5pRoot, flatFileName), }) } } }) return arrayOfFiles } const filesToCopy = getAllFiles(argument.path) // Copy each file to the root level (no directories) for (const file of filesToCopy) { await fs.copy(file.source, file.target) } console.log(`Copied ${filesToCopy.length} resource files to H5P package`) } catch (error) { console.warn('Error copying content files:', error) } } // Package as .h5p file if (argument['h5p-zip'] !== false) { // Default to creating .h5p file const output = fs.createWriteStream(argument.output + '.h5p') const archive = archiver('zip', { zlib: { level: 9 }, }) output.on('close', function () { console.log(archive.pointer() + ' total bytes') console.log( 'H5P package created successfully: ' + argument.output + '.h5p' ) }) archive.on('error', function (err) { throw err }) archive.pipe(output) // Add all files from the h5pRoot directory to the archive archive.directory(h5pRoot, false) archive.finalize() } else { // Otherwise just copy the directory await fs.copy(h5pRoot, argument.output) console.log('H5P content created successfully at: ' + argument.output) } } // Helper function to convert markdown to HTML function convertMarkdownToHTML(markdown) { // This is a very basic markdown to HTML converter // In a real implementation, you would use a proper markdown parser let html = markdown // Convert headers html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>') html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>') html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>') html = html.replace(/^#### (.*?)$/gm, '<h4>$1</h4>') html = html.replace(/^##### (.*?)$/gm, '<h5>$1</h5>') html = html.replace(/^###### (.*?)$/gm, '<h6>$1</h6>') // Convert bold and italic html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') html = html.replace(/\*(.*?)\*/g, '<em>$1</em>') // Convert links html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>') // Convert code blocks html = html.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>') // Convert inline code html = html.replace(/`(.*?)`/g, '<code>$1</code>') // Convert lists const lines = html.split('\n') let inList = false let listType = '' for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^- /)) { if (!inList || listType !== 'ul') { lines[i] = inList ? '</ul><ul><li>' + lines[i].substring(2) + '</li>' : '<ul><li>' + lines[i].substring(2) + '</li>' inList = true listType = 'ul' } else { lines[i] = '<li>' + lines[i].substring(2) + '</li>' } } else if (lines[i].match(/^\d+\. /)) { if (!inList || listType !== 'ol') { lines[i] = inList ? '</ol><ol><li>' + lines[i].substring(lines[i].indexOf('. ') + 2) + '</li>' : '<ol><li>' + lines[i].substring(lines[i].indexOf('. ') + 2) + '</li>' inList = true listType = 'ol' } else { lines[i] = '<li>' + lines[i].substring(lines[i].indexOf('. ') + 2) + '</li>' } } else if (inList && lines[i].trim() === '') { lines[i] = listType === 'ul' ? '</ul>' : '</ol>' inList = false } else if (inList) { // This line is part of a list item lines[i] = lines[i] } else { // Regular paragraph if (lines[i].trim() !== '') { if (i === 0 || lines[i - 1].trim() === '') { lines[i] = '<p>' + lines[i] + '</p>' } else if (!lines[i - 1].endsWith('</p>')) { // Continue paragraph lines[i] = lines[i] } else { // New paragraph lines[i] = '<p>' + lines[i] + '</p>' } } } } if (inList) { lines.push(listType === 'ul' ? '</ul>' : '</ol>') } html = lines.join('\n') // Add CSS for styling html = `<style> .liascript-content { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; } .markdown-content { background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; padding: 20px; margin-bottom: 20px; } .download-section { background-color: #e9f7fe; border: 1px solid #a8d7f3; border-radius: 5px; padding: 15px; margin-top: 30px; } code { background-color: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-family: monospace; } pre { background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow-x: auto; } pre code { background-color: transparent; padding: 0; } a { color: #0066cc; text-decoration: none; } a:hover { text-decoration: underline; } </style>` + html return html } // Update the function signature to accept markdownContent async function createSelfContainedHtml(indexPath, basePath, markdownContent) { const cheerio = require('cheerio') const fs = require('fs-extra') const path = require('path') const mime = require('mime-types') // Read the index HTML file const htmlContent = await fs.readFile(indexPath, 'utf8') const $ = cheerio.load(htmlContent) // Process and inline JavaScript files $('script[src]').each(function () { const src = $(this).attr('src') if ( src.startsWith('http:') || src.startsWith('https:') || src.startsWith('//') ) { // Skip external scripts return } try { const scriptPath = path.resolve(path.dirname(indexPath), src) const scriptContent = fs.readFileSync(scriptPath, 'utf8') $(this).removeAttr('src').text(scriptContent) console.log(`Inlined script: ${src}`) } catch (error) { console.warn(`Could not inline script ${src}:`, error.message) } }) // Process and inline CSS files $('link[rel="stylesheet"]').each(function () { const href = $(this).attr('href') if ( href.startsWith('http:') || href.startsWith('https:') || href.startsWith('//') ) { // Skip external stylesheets return } try { const cssPath = path.resolve(path.dirname(indexPath), href) const cssContent = fs.readFileSync(cssPath, 'utf8') $(this).replaceWith(`<style>${cssContent}</style>`) console.log(`Inlined CSS: ${href}`) } catch (error) { console.warn(`Could not inline CSS ${href}:`, error.message) } }) // Process and inline images (convert to data URLs) $('img[src]').each(function () { const src = $(this).attr('src') if ( src.startsWith('data:') || src.startsWith('http:') || src.startsWith('https:') || src.startsWith('//') ) { // Skip already inlined or external images return } try { const imgPath = path.resolve(path.dirname(indexPath), src) const imgBuffer = fs.readFileSync(imgPath) const mimeType = mime.lookup(imgPath) || 'application/octet-stream' const dataUrl = `data:${mimeType};base64,${imgBuffer.toString('base64')}` $(this).attr('src', dataUrl) console.log(`Inlined image: ${src}`) } catch (error) { console.warn(`Could not inline image ${src}:`, error.message) } }) // Inject the markdown content directly $('#container').append(` <div id="embedded-markdown" style="display:none;"> ${markdownContent.replace(/</g, '&lt;').replace(/>/g, '&gt;')} </div> `) // Add a script to process the embedded markdown on page load $('body').append(` <script> document.addEventListener('DOMContentLoaded', function() { if (window.LIA && typeof window.LIA.loadMarkdown === 'function') { const markdown = document.getElementById('embedded-markdown').innerHTML .replace(/&lt;/g, '<') .replace(/&gt;/g, '>'); window.LIA.loadMarkdown(markdown); } }); </script> `) return $.html() }