diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index ce423975f..3a1f34446 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -15,10 +15,10 @@ RUN npm install -g pnpm@8.15.9 && pnpm config set store-dir /tmp/cache/pnpm COPY pnpm-lock.yaml .npmrc /app/ # COPY patches /app/patches RUN --mount=type=cache,target=/tmp/cache pnpm fetch && \ - # There is a bug in pnpm: `pnpm fetch` creates _some_ node_modules folders - # with _some_ packages. This can lead to an incomplete package installation. - # So we remove them now. - find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + + # There is a bug in pnpm: `pnpm fetch` creates _some_ node_modules folders + # with _some_ packages. This can lead to an incomplete package installation. + # So we remove them now. + find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + # Install composer dependencies. # They may contain directive definitions required by prep scripts. @@ -43,9 +43,9 @@ ENV VITE_DECAP_BRANCH="$LAGOON_GIT_BRANCH" # Copy the all package sources, install and prepare them. COPY . /app RUN --mount=type=cache,target=/tmp/cache pnpm i && \ - pnpm turbo:prep && \ - # Remove all node_modules to reduce the size of the image. - find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + + pnpm turbo:prep && \ + # Remove all node_modules to reduce the size of the image. + find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + # Deploy apps. RUN --mount=type=cache,target=/tmp/cache pnpm deploy --filter "@custom/cms" /tmp/.deploy/cms --prod @@ -124,3 +124,13 @@ ARG LAGOON_GIT_BRANCH ENV VITE_DECAP_BRANCH="$LAGOON_GIT_BRANCH" CMD pnpm publisher + +# ==================================================================================================== +# CONVERTER IMAGE +# ==================================================================================================== + +FROM uselagoon/node-18 as convertmd + +RUN npm install -g pnpm@8.15.9 +COPY --from=builder /tmp/.deploy/converter /app +CMD pnpm start diff --git a/apps/converter/README.md b/apps/converter/README.md new file mode 100644 index 000000000..108702c7a --- /dev/null +++ b/apps/converter/README.md @@ -0,0 +1,107 @@ +# Silverback Converter + +The converter is a Node.js application designed to convert documents from +various formats (DocX, PDF, and HTML) into Markdown. + +This tool is particularly useful for developers and content creators who need to +transform documents into a format suitable for further processing, analysis, or +integration with other systems. + +## Features + +- **DocX to Markdown**: Convert Word documents (`.docx`) to Markdown. +- **PDF to Markdown**: Convert PDF files to Markdown. +- **HTML to Markdown**: Extract main content from web pages and convert it to + Markdown. +- **Jina AI Integration**: Fetch and convert content using the Jina AI API. + (ATTENTION: EXPERIMENTAL, DO NOT USE THIS) + +## Setup and Installation + +### Prerequisites + +- Node.js (version 18 or higher) +- npm (Node Package Manager) + +### Installation + +1. **Install dependencies**: + + ```bash + npm i + ``` + +2. **Set up environment variables** (optional): + - Create a `.env` file in the root directory. + - Add your Jina AI API key if you plan to use the Jina AI integration: + ```env + JINA_AI_API_KEY=your_jina_ai_api_key + ``` + +### Running the Application + +To start the application, run the following command: + +```bash +npm start +``` + +The server will start on `http://localhost:3000`. + +## Usage + +### Endpoints + +- **Convert DocX to Markdown**: + + ``` + GET /convert?path=/path/to/your/document.docx + ``` + +- **Convert PDF to Markdown**: + + ``` + GET /pdf-convert?path=/path/to/your/document.pdf + ``` + +- **Convert HTML to Markdown**: + + ``` + GET /html-convert?path=https://example.com + ``` + +- **Fetch and Convert Content with Jina AI**: + ``` + GET /jina-convert?path=https://example.com + ``` + +### Example + +To convert a Word document to Markdown, make a GET request to: + +``` +http://localhost:3000/convert?path=/path/to/your/document.docx +``` + +The response will include the converted Markdown content, the output directory, +and any warnings generated during the conversion process. + +## Configuration + +- **Output Directory**: By default, converted files are saved in a directory + named after the input file's hash. You can customize the output directory by + modifying the `outputDir` variable in the respective conversion scripts. +- **Image Handling**: Images extracted from documents are saved in an `images` + subdirectory within the output directory. + +## Dependencies + +The application relies on several npm packages, including: + +- `mammoth` for DocX conversion +- `@opendocsg/pdf2md` for PDF conversion +- `@extractus/article-extractor` for HTML content extraction +- `turndown` for HTML to Markdown conversion +- `express` for the server + +For a complete list of dependencies, refer to the `package.json` file. diff --git a/apps/converter/htmlToMarkdown.js b/apps/converter/htmlToMarkdown.js new file mode 100644 index 000000000..c9abd1fbe --- /dev/null +++ b/apps/converter/htmlToMarkdown.js @@ -0,0 +1,216 @@ +import { extract } from '@extractus/article-extractor'; +import crypto from 'crypto'; +import fs from 'fs-extra'; +import imageType from 'image-type'; +import { JSDOM } from 'jsdom'; +import { applyFixes } from 'markdownlint'; +import { lint as lintSync } from 'markdownlint/sync'; +import fetch from 'node-fetch'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { + convertToMarkdown, + generateFolderName, + validateAndFixMarkdown, +} from './utils/utils.js'; + +/** + * Extracts images from markdown content while preserving their positions + * @param {string} markdown - Original markdown content + * @returns {{cleanMarkdown: string, extractedImages: Array<{alt: string, url: string, position: number, placeholder: string}>}} + */ +function extractImagesWithPositions(markdown) { + const imageRegex = /!\[(.*?)\]\((.*?)\)/g; + const extractedImages = []; + let match; + let cleanMarkdown = markdown; + let index = 0; + + while ((match = imageRegex.exec(markdown)) !== null) { + const placeholder = `__IMAGE_PLACEHOLDER_${index}__`; + extractedImages.push({ + alt: match[1] || '', + url: match[2], + position: match.index, + placeholder, + }); + index++; + } + + // Replace images with placeholders + extractedImages.forEach((image) => { + cleanMarkdown = cleanMarkdown.replace( + `![${image.alt}](${image.url})`, + image.placeholder, + ); + }); + + return { + cleanMarkdown, + extractedImages, + }; +} + +/** + * Reinserts images just above their original link positions + * @param {string} markdown - Markdown content with placeholders + * @param {Array<{alt: string, url: string, placeholder: string}>} images - Extracted images + * @returns {string} - Markdown with images reinserted + */ +function reinsertImages(markdown, images) { + let result = markdown; + + // Sort images by their position in reverse order to maintain correct positions + const sortedImages = [...images].sort((a, b) => b.position - a.position); + + for (const image of sortedImages) { + const imageMarkdown = `![${image.alt}](${image.url})\n\n`; + const placeholderPosition = result.indexOf(image.placeholder); + + if (placeholderPosition !== -1) { + // Find the start of the line containing the placeholder + let lineStart = result.lastIndexOf('\n', placeholderPosition); + lineStart = lineStart === -1 ? 0 : lineStart + 1; + + // Insert the image above the line containing the placeholder + result = + result.slice(0, lineStart) + imageMarkdown + result.slice(lineStart); + + // Remove the placeholder + result = result.replace(image.placeholder, ''); + } + } + + // Clean up any double blank lines created during the process + result = result.replace(/\n{3,}/g, '\n\n'); + + return result.trim(); +} + +// @todo Fix this to work locally and live +const isLagoon = !!process.env.LAGOON; +const __dirname = isLagoon + ? '/app/web/sites/default/files/converted' + : '/tmp/converted'; + +async function extractMainContentFromUrl(url) { + try { + const mainContent = await extract(url); + return mainContent ? mainContent.content : ''; + } catch (err) { + console.error(err); + } + return ''; +} + +async function getImageExtension(buffer) { + const type = await imageType(buffer); + return type ? `.${type.ext}` : '.png'; +} + +async function downloadImage(url) { + try { + const response = await fetch(url); + if (!response.ok) + throw new Error(`Failed to fetch image: ${response.statusText}`); + return Buffer.from(await response.arrayBuffer()); + } catch (error) { + console.warn( + `Warning: Failed to download image from ${url}:`, + error.message, + ); + return null; + } +} + +function isValidUrl(string) { + try { + new URL(string); + return true; + } catch (_) { + return false; + } +} + +export async function htmlToMarkdown(url) { + if (!isValidUrl(url)) { + throw new Error('Invalid URL provided: ' + url); + } + + const html = await extractMainContentFromUrl(url); + // Generate folder name based on HTML content + const folderName = generateFolderName(url); + const outputDir = path.join(__dirname, folderName); + const imagesDir = path.join(outputDir, 'images'); + + await fs.ensureDir(outputDir); + await fs.ensureDir(imagesDir); + + // Parse HTML using JSDOM + const dom = new JSDOM(html); + const document = dom.window.document; + + // Process images before conversion + const images = document.querySelectorAll('img'); + const imageMap = new Map(); + + for (const img of images) { + const srcAttribute = img.getAttribute('src'); + if (!srcAttribute) continue; + + // Resolve relative URLs to absolute URLs + const absoluteUrl = new URL(srcAttribute, url).href; + + const imageBuffer = await downloadImage(absoluteUrl); + if (!imageBuffer) continue; + + const extension = await getImageExtension(imageBuffer); + const filename = `image-${crypto.randomBytes(4).toString('hex')}${extension}`; + const imagePath = path.join(imagesDir, filename); + + await fs.writeFile(imagePath, imageBuffer); + imageMap.set(srcAttribute, path.join('images', filename)); + img.setAttribute('src', path.join('images', filename)); + } + + // Convert to Markdown + let markdown = convertToMarkdown(document.body); + + // Clean up the markdown + markdown = markdown + .replace(/\n\s*\n\s*\n/g, '\n\n') + .replace(/!\[\]\(/g, '![image](') + .trim(); + + const results = lintSync({ strings: { content: markdown } }); + const fixed = applyFixes(markdown, results.content); + const { markdown: fixedMarkdown, warnings } = validateAndFixMarkdown(fixed); + + const { cleanMarkdown, extractedImages } = + extractImagesWithPositions(fixedMarkdown); + const correctedMarkdown = reinsertImages(cleanMarkdown, extractedImages); + + const fixEmptyMarkdownLinks = (markdown) => { + // Regular expression to match markdown links with empty URL but with title + // Captures: []("title") + const emptyLinkRegex = /\[\]\(([^)]+)\s+"([^"]+)"\)/g; + + // Replace empty links with their title text as link text + return markdown.replace(emptyLinkRegex, (match, url, title) => { + return `[${title}](${url} "${title}")`; + }); + }; + + const fixedLinksMarkdown = fixEmptyMarkdownLinks(correctedMarkdown); + + // Save markdown file + const mdPath = path.join(outputDir, 'content.md'); + await fs.writeFile(mdPath, fixedLinksMarkdown); + + return { + markdownPath: mdPath, + warnings: warnings, + outputDir, + }; +} diff --git a/apps/converter/index.js b/apps/converter/index.js new file mode 100644 index 000000000..abbfc285e --- /dev/null +++ b/apps/converter/index.js @@ -0,0 +1,288 @@ +import { parse } from '@textlint/markdown-to-ast'; +import express from 'express'; +import { readFileSync } from 'fs'; +import { toHtml } from 'hast-util-to-html'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { toHast } from 'mdast-util-to-hast'; + +import { htmlToMarkdown } from './htmlToMarkdown.js'; +import { fetchContentJinaAi } from './jinaAi.js'; +import { pdfToMarkdown } from './pdfToMarkdown.js'; +import { wordToMarkdown } from './wordToMarkdown.js'; + +const app = express(); +const PORT = 3000; + +async function enhanceMdastNodesRecursive(tree, outputDir) { + // Process a single node and its children + async function processNode(node) { + // First process all children recursively to ensure they have htmlValue + if (node.children && Array.isArray(node.children)) { + await Promise.all(node.children.map((child) => processNode(child))); + } + + const hast = toHast(node, { allowDangerousHtml: true }); + const html = toHtml(hast, { allowDangerousHtml: true }); + + const type = node.type; + node.type = type.charAt(0).toUpperCase() + type.slice(1); + node.outputDir = outputDir; + + if (!node.htmlValue) { + node.htmlValue = html; + } + + if (node.type == 'Table') { + node.htmlValue = markdownToHtmlTable(html); + } + + if (node.type == 'Image') { + node.src = `${outputDir}/${node.url}`; + } + + return node; + } + + return processNode(tree); +} + +function markdownToHtmlTable(markdownTable) { + // Split the markdown table into lines + const lines = markdownTable.trim().split('\n'); + + // Extract headers (first line) + const headers = lines[0] + .split('|') + .map((header) => header.trim()) + .map((header) => header.replace(/^

/, '').replace(/<\/p>$/, '')) + .filter((header) => header !== ''); + + // Remove separator line (second line with ---) + const dataLines = lines.slice(2); + + // Create HTML table + let htmlTable = '\n\n'; + + // Add headers + headers.forEach((header) => { + htmlTable += `\n`; + }); + + htmlTable += '\n\n\n'; + + // Add table rows + dataLines.forEach((line) => { + const cells = line + .split('|') + .map((cell) => cell.trim()) + .map((cell) => cell.replace(/^

/, '').replace(/<\/p>$/, '')) + .filter((cell) => cell !== ''); + + if (cells.length > 0) { + htmlTable += '\n

'; + cells.forEach((cell) => { + htmlTable += `\n`; + }); + htmlTable += '\n'; + } + }); + htmlTable += '\n\n
${header}
${cell}
'; + + return htmlTable; +} + +// Express endpoint +app.get('/convert', async (req, res) => { + const filePath = req.query.path; + + if (!filePath) { + return res.status(400).json({ + error: "Please provide a Word document path as 'path' query parameter", + }); + } + + try { + // First convert Word to Markdown + const { markdownPath, warnings, outputDir } = + await wordToMarkdown(filePath); + + // Then read and process the Markdown + const markdown = readFileSync(markdownPath, 'utf-8'); + const mdast = fromMarkdown(markdown); + const md = readFileSync(markdownPath, 'utf-8'); + const ast = parse(md); + + // This is to correct some types + mdast.children.forEach(async (element, index) => { + const hast = toHast(element, { allowDangerousHtml: true }); + const html = toHtml(hast, { allowDangerousHtml: true }); + element.type = ast.children[index].type; + element.raw = ast.children[index].raw; + element.htmlValue = html; + }); + + const enhanced = await enhanceMdastNodesRecursive(mdast, outputDir); + + // Return the processed content along with conversion info + res.json({ + content: enhanced.children, + outputDirectory: outputDir, + warnings: warnings, + }); + } catch (error) { + if (error.code === 'ENOENT') { + res.status(404).json({ error: `File not found: ${filePath}` }); + } else { + res.status(500).json({ + error: 'Error processing document', + details: error.message, + }); + } + } +}); + +app.get('/html-convert', async (req, res) => { + const filePath = req.query.path; + + if (!filePath) { + return res.status(400).json({ + error: "Please provide a URLas 'path' query parameter", + }); + } + + try { + // First convert Word to Markdown + const { markdownPath, warnings, outputDir } = + await htmlToMarkdown(filePath); + + // Then read and process the Markdown + const markdown = readFileSync(markdownPath, 'utf-8'); + const mdast = fromMarkdown(markdown); + + const md = readFileSync(markdownPath, 'utf-8'); + const ast = parse(md); + + mdast.children.forEach(async (element, index) => { + const hast = toHast(element, { allowDangerousHtml: true }); + const html = toHtml(hast, { allowDangerousHtml: true }); + element.type = ast.children[index].type; + element.raw = ast.children[index].raw; + element.htmlValue = html; + }); + + const enhanced = await enhanceMdastNodesRecursive(mdast, outputDir); + // Return the processed content along with conversion info + res.json({ + content: enhanced.children, + outputDirectory: outputDir, + warnings: warnings, + }); + } catch (error) { + if (error.code === 'ENOENT') { + res.status(404).json({ error: `File not found: ${filePath}` }); + } else { + res.status(500).json({ + error: 'Error processing document', + details: error.message, + }); + } + } +}); + +app.get('/jina-convert', async (req, res) => { + const url = req.query.path; + + if (!url) { + return res.status(400).json({ + error: "Please provide a URLas 'path' query parameter", + }); + } + + try { + // First convert Word to Markdown + const { markdownPath, warnings, outputDir } = await fetchContentJinaAi(url); + + // Then read and process the Markdown + const markdown = readFileSync(markdownPath, 'utf-8'); + const mdast = fromMarkdown(markdown); + + const md = readFileSync(markdownPath, 'utf-8'); + const ast = parse(md); + + mdast.children.forEach(async (element, index) => { + const hast = toHast(element, { allowDangerousHtml: true }); + const html = toHtml(hast, { allowDangerousHtml: true }); + element.type = ast.children[index].type; + element.raw = ast.children[index].raw; + element.htmlValue = html; + }); + + const enhanced = await enhanceMdastNodesRecursive(mdast, outputDir); + // Return the processed content along with conversion info + res.json({ + content: enhanced.children, + outputDirectory: outputDir, + warnings: warnings, + }); + } catch (error) { + if (error.code === 'ENOENT') { + res.status(404).json({ error: `File not found: ${url}` }); + } else { + res.status(500).json({ + error: 'Error processing document', + details: error.message, + }); + } + } +}); + +app.get('/pdf-convert', async (req, res) => { + const filePath = req.query.path; + + if (!filePath) { + return res.status(400).json({ + error: "Please provide a URLas 'path' query parameter", + }); + } + + try { + // First convert Word to Markdown + const { markdownPath, warnings, outputDir } = await pdfToMarkdown(filePath); + + // Then read and process the Markdown + const markdown = readFileSync(markdownPath, 'utf-8'); + const mdast = fromMarkdown(markdown); + + const md = readFileSync(markdownPath, 'utf-8'); + const ast = parse(md); + + mdast.children.forEach(async (element, index) => { + const hast = toHast(element, { allowDangerousHtml: true }); + const html = toHtml(hast, { allowDangerousHtml: true }); + element.type = ast.children[index].type; + element.raw = ast.children[index].raw; + element.htmlValue = html; + }); + + const enhanced = await enhanceMdastNodesRecursive(mdast, outputDir); + // Return the processed content along with conversion info + res.json({ + content: enhanced.children, + outputDirectory: outputDir, + warnings: warnings, + }); + } catch (error) { + if (error.code === 'ENOENT') { + res.status(404).json({ error: `File not found: ${filePath}` }); + } else { + res.status(500).json({ + error: 'Error processing document', + details: error.message, + }); + } + } +}); + +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); diff --git a/apps/converter/jinaAi.js b/apps/converter/jinaAi.js new file mode 100644 index 000000000..371d9d223 --- /dev/null +++ b/apps/converter/jinaAi.js @@ -0,0 +1,86 @@ +import fs from 'fs-extra'; +import { applyFixes } from 'markdownlint'; +import { lint as lintSync } from 'markdownlint/sync'; +import fetch from 'node-fetch'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { generateFolderName, validateAndFixMarkdown } from './utils/utils.js'; + +const isLagoon = !!process.env.LAGOON; +const __filename = fileURLToPath(import.meta.url); +const __dirname = isLagoon + ? '/app/web/sites/default/files/converted' + : path.dirname(__filename); + +export async function fetchContentJinaAi(url) { + const apiKey = + process.env.JINA_AI_API_KEY || + 'jina_c436e2d8a5474a71b232f4286de387d6n0MVWKn1aOY3BNfVGE0gJH300OI0'; + + try { + // Encode the URL to handle special characters + const encodedUrl = encodeURIComponent(url); + + // Make the request to the Jina API + const response = await fetch(`https://r.jina.ai/${encodedUrl}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: 'application/json', + }, + }); + + // Check if the response is ok + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Parse and return the response + const fetchedContent = await response.json(); + + const folderName = generateFolderName(url); + const outputDir = path.join(__dirname, folderName); + const imagesDir = path.join(outputDir, 'images'); + + await fs.ensureDir(outputDir); + await fs.ensureDir(imagesDir); + + let markdown = fetchedContent?.data.content || ''; + + // Clean up the markdown + markdown = markdown + .replace(/\n\s*\n\s*\n/g, '\n\n') + .replace(/!\[\]\(/g, '![image](') + .trim(); + + const results = lintSync({ strings: { content: markdown } }); + const fixed = applyFixes(markdown, results.content); + const { markdown: fixedMarkdown, warnings } = validateAndFixMarkdown(fixed); + + const fixEmptyMarkdownLinks = (markdown) => { + // Regular expression to match markdown links with empty URL but with title + // Captures: []("title") + const emptyLinkRegex = /\[\]\(([^)]+)\s+"([^"]+)"\)/g; + + // Replace empty links with their title text as link text + return markdown.replace(emptyLinkRegex, (match, url, title) => { + return `[${title}](${url} "${title}")`; + }); + }; + + const fixedLinksMarkdown = fixEmptyMarkdownLinks(fixedMarkdown); + + const mdPath = path.join(outputDir, 'content.md'); + await fs.writeFile(mdPath, fixedLinksMarkdown); + + return { + markdownPath: mdPath, + warnings: warnings, + outputDir, + }; + } catch (error) { + console.error('Error fetching content:', error); + throw error; + } +} diff --git a/apps/converter/package-lock.json b/apps/converter/package-lock.json new file mode 100644 index 000000000..fbc3bb1b4 --- /dev/null +++ b/apps/converter/package-lock.json @@ -0,0 +1,4118 @@ +{ + "name": "@custom/converter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@custom/converter", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@extractus/article-extractor": "^8.0.16", + "@textlint/markdown-to-ast": "^14.3.0", + "dotenv": "^16.4.7", + "express": "^4.21.1", + "fs-extra": "^11.2.0", + "hast-util-to-html": "^9.0.3", + "image-type": "^5.2.0", + "jsdom": "^25.0.1", + "langchain": "^0.3.6", + "mammoth": "^1.8.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-to-hast": "^13.2.0", + "node-fetch": "^3.3.2", + "openai": "^4.76.1", + "pdf-parse": "github:iamh2o/pdf-parse#1.1.3", + "pdf2json": "^3.1.4", + "sanitize-filename": "^1.6.3", + "turndown": "^7.2.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.0.3.tgz", + "integrity": "sha512-ZykIcDTVv5UNmKWSTLAs3VukO6NDJkkSKxrgUTDPBkAlORVT3H9n5DbRjRl8xIotklscHdbLIa0b9+y3mQq73g==", + "peer": true + }, + "node_modules/@extractus/article-extractor": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/@extractus/article-extractor/-/article-extractor-8.0.16.tgz", + "integrity": "sha512-amxCKO2uerY0UPxDVSoTDdcTny0otpKsAIGC2q2CUDEhUX6EfxmpURttlKLx9uWFT9DRlNX9LSyMSP/2p7kFLg==", + "dependencies": { + "@mozilla/readability": "^0.5.0", + "bellajs": "^11.2.0", + "cross-fetch": "^4.0.0", + "linkedom": "^0.18.5", + "sanitize-html": "2.13.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@langchain/core": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.23.tgz", + "integrity": "sha512-Aut43dEJYH/ibccSErFOLQzymkBG4emlN16P0OHWwx02bDosOR9ilZly4JJiCSYcprn2X2H8nee6P/4VMg1oQA==", + "peer": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.2.8", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/openai": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.14.tgz", + "integrity": "sha512-lNWjUo1tbvsss45IF7UQtMu1NJ6oUKvhgPYWXnX9f/d6OmuLu7D99HQ3Y88vLcUo9XjjOy417olYHignMduMjA==", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.71.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.26 <0.4.0" + } + }, + "node_modules/@langchain/textsplitters": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", + "integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==", + "dependencies": { + "js-tiktoken": "^1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==" + }, + "node_modules/@mozilla/readability": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz", + "integrity": "sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.3.0.tgz", + "integrity": "sha512-baDgKcA8MeO55I2+LNc9FTAJ/aUKlxN6DgM5B511tT9kDwECXRk+iYi/H+oaP25z5Zq3FqrL6n7mmyfFWDUWkQ==" + }, + "node_modules/@textlint/markdown-to-ast": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-14.3.0.tgz", + "integrity": "sha512-z4UMKFh3r5KtylPt5OO6su7DScU+fMZ7Qv5LTrJNaOqcmOzFho64Y1I26BJv86f8BC+MUYP0kza5MZGaR2LYQA==", + "dependencies": { + "@textlint/ast-node-types": "^14.3.0", + "debug": "^4.3.4", + "mdast-util-gfm-autolink-literal": "^0.1.3", + "neotraverse": "^0.6.15", + "remark-footnotes": "^3.0.0", + "remark-frontmatter": "^3.0.0", + "remark-gfm": "^1.0.0", + "remark-parse": "^9.0.0", + "unified": "^9.2.2" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bellajs": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/bellajs/-/bellajs-11.2.0.tgz", + "integrity": "sha512-Wjss+Bc674ZABPr+SCKWTqA4V1pyYFhzDTjNBJy4jdmgOv0oGIGXeKBRJyINwP5tIy+iIZD9SfgZpztduzQ5QA==", + "engines": { + "node": ">= 18.4" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", + "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/image-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/image-type/-/image-type-5.2.0.tgz", + "integrity": "sha512-f0+6qHeGfyEh1HhFGPUWZb+Dqqm6raKeeAR6Opt01wBBIQL32/1wpZkPQm8gcliB/Ws6oiX2ofFYXB57+CV0iQ==", + "dependencies": { + "file-type": "^18.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/js-tiktoken": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz", + "integrity": "sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/langchain": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.6.tgz", + "integrity": "sha512-erZOIKXzwCOrQHqY9AyjkQmaX62zUap1Sigw1KrwMUOnVoLKkVNRmAyxFlNZDZ9jLs/58MaQcaT9ReJtbj3x6w==", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.4.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langsmith": "^0.2.0", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^10.0.0", + "yaml": "^2.2.1", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.2.21 <0.4.0", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", + "axios": "*", + "cheerio": "*", + "handlebars": "^4.7.8", + "peggy": "^3.0.2", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/aws": { + "optional": true + }, + "@langchain/cohere": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/google-vertexai": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "peggy": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, + "node_modules/langsmith": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.2.11.tgz", + "integrity": "sha512-rVPUN/jQEHjTuYaoVKGjfb3NsYNLGTQT9LXcgJvka5M0EDcXciC598A+DsAQrl6McdfSJCFJDelgRPqVoF2xNA==", + "dependencies": { + "@types/uuid": "^10.0.0", + "commander": "^10.0.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkedom": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.5.tgz", + "integrity": "sha512-JGLaGGtqtu+eOhYrC1wkWYTBcpVWL4AsnwAtMtgO1Q0gI0PuPJKI0zBBE+a/1BrhOE3Uw8JI/ycByAv5cLrAuQ==", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^9.1.0", + "uhyphen": "^0.2.0" + } + }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/mammoth": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.8.0.tgz", + "integrity": "sha512-pJNfxSk9IEGVpau+tsZFz22ofjUsl2mnA5eT8PjPs2n0BP+rhVte4Nez6FdgEuxv3IGI3afiV46ImKqTGDVlbA==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz", + "integrity": "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-footnote": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/mdast-util-footnote/-/mdast-util-footnote-0.1.7.tgz", + "integrity": "sha512-QxNdO8qSxqbO2e3m09KwDKfWiLgqyCurdWTQ198NpbZ2hxntdc+VKS4fDJCmNWbAroUdYnSthu+XbZ8ovh8C3w==", + "dependencies": { + "mdast-util-to-markdown": "^0.6.0", + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-footnote/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-0.2.0.tgz", + "integrity": "sha512-FHKL4w4S5fdt1KjJCwB0178WJ0evnyyQr5kXTM3wrOVpytD0hrkvd+AOOjU9Td8onOejCkmZ+HQRT3CZ3coHHQ==", + "dependencies": { + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz", + "integrity": "sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==", + "dependencies": { + "mdast-util-gfm-autolink-literal": "^0.1.0", + "mdast-util-gfm-strikethrough": "^0.2.0", + "mdast-util-gfm-table": "^0.1.0", + "mdast-util-gfm-task-list-item": "^0.1.0", + "mdast-util-to-markdown": "^0.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz", + "integrity": "sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==", + "dependencies": { + "ccount": "^1.0.0", + "mdast-util-find-and-replace": "^1.1.0", + "micromark": "^2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz", + "integrity": "sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==", + "dependencies": { + "mdast-util-to-markdown": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz", + "integrity": "sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==", + "dependencies": { + "markdown-table": "^2.0.0", + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz", + "integrity": "sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==", + "dependencies": { + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", + "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", + "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-footnote": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/micromark-extension-footnote/-/micromark-extension-footnote-0.3.2.tgz", + "integrity": "sha512-gr/BeIxbIWQoUm02cIfK7mdMZ/fbroRpLsck4kvFtjbzP4yi+OPVbnukTc/zy0i7spC2xYE/dbX1Sur8BEDJsQ==", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-footnote/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-0.2.2.tgz", + "integrity": "sha512-q6nPLFCMTLtfsctAuS0Xh4vaolxSFUWUWR6PZSrXXiRy+SANGllpcqdXFv2z07l0Xz/6Hl40hK0ffNCJPH2n1A==", + "dependencies": { + "fault": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz", + "integrity": "sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==", + "dependencies": { + "micromark": "~2.11.0", + "micromark-extension-gfm-autolink-literal": "~0.5.0", + "micromark-extension-gfm-strikethrough": "~0.6.5", + "micromark-extension-gfm-table": "~0.4.0", + "micromark-extension-gfm-tagfilter": "~0.3.0", + "micromark-extension-gfm-task-list-item": "~0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz", + "integrity": "sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==", + "dependencies": { + "micromark": "~2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz", + "integrity": "sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz", + "integrity": "sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz", + "integrity": "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz", + "integrity": "sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.2.tgz", + "integrity": "sha512-xKxhkB62vwHUuuxHe9Xqty3UaAsizV2YKq5OV344u3hFBbf8zIYrhYOWhAQb94MtMPkjTOzzjJ/hid9/dR5vFA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==" + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==" + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openai": { + "version": "4.76.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.76.1.tgz", + "integrity": "sha512-ci63/WFEMd6QjjEVeH0pV7hnFS6CCqhgJydSti4Aak/8uo2SpgzKjteUDaY+OkwziVj11mi6j+0mRUIiGKUzWw==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/openai/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/openai/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "node_modules/pdf-parse": { + "version": "1.1.3", + "resolved": "git+ssh://git@github.com/iamh2o/pdf-parse.git#d7a41d5aaed1503bee2d7ea50bf89588d3b2d2cf", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + } + }, + "node_modules/pdf-parse/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/pdf2json": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/pdf2json/-/pdf2json-3.1.4.tgz", + "integrity": "sha512-rS+VapXpXZr+5lUpHmRh3ugXdFXp24p1RyG24yP1DMpqP4t0mrYNGpLtpSbWD42PnQ59GIXofxF+yWb7M+3THg==", + "bundleDependencies": [ + "@xmldom/xmldom" + ], + "dependencies": { + "@xmldom/xmldom": "^0.8.10" + }, + "bin": { + "pdf2json": "bin/pdf2json.js" + }, + "engines": { + "node": ">=18.12.1", + "npm": ">=8.19.2" + } + }, + "node_modules/pdf2json/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/peek-readable": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz", + "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/remark-footnotes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-3.0.0.tgz", + "integrity": "sha512-ZssAvH9FjGYlJ/PBVKdSmfyPc3Cz4rTWgZLI4iE/SX8Nt5l3o3oEjv3wwG5VD7xOjktzdwp5coac+kJV9l4jgg==", + "dependencies": { + "mdast-util-footnote": "^0.1.0", + "micromark-extension-footnote": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-3.0.0.tgz", + "integrity": "sha512-mSuDd3svCHs+2PyO29h7iijIZx4plX0fheacJcAoYAASfgzgVIcXGYSq9GFyYocFLftQs8IOmmkgtOovs6d4oA==", + "dependencies": { + "mdast-util-frontmatter": "^0.2.0", + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", + "integrity": "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==", + "dependencies": { + "mdast-util-gfm": "^0.1.0", + "micromark-extension-gfm": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-parse/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/remark-parse/node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sanitize-html": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz", + "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strtok3": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz", + "integrity": "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.1.3" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/tldts": { + "version": "6.1.66", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.66.tgz", + "integrity": "sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==", + "dependencies": { + "tldts-core": "^6.1.66" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.66", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.66.tgz", + "integrity": "sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/turndown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", + "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/unified/node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/unist-util-visit/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz", + "integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==", + "peerDependencies": { + "zod": "^3.23.3" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/apps/converter/package.json b/apps/converter/package.json new file mode 100644 index 000000000..4f3f2dddb --- /dev/null +++ b/apps/converter/package.json @@ -0,0 +1,38 @@ +{ + "name": "@custom/converter", + "version": "1.0.0", + "description": "A custom converter from multiple formats to markdown.", + "main": "index.js", + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@extractus/article-extractor": "^8.0.16", + "@opendocsg/pdf2md": "^0.2.1", + "@textlint/markdown-to-ast": "^14.3.0", + "axios": "^1.7.9", + "dotenv": "^16.4.7", + "express": "^4.21.1", + "fs-extra": "^11.2.0", + "hast-util-to-html": "^9.0.3", + "image-type": "^5.2.0", + "install": "^0.13.0", + "jsdom": "^25.0.1", + "mammoth": "^1.8.0", + "markdownlint": "^0.37.1", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-to-hast": "^13.2.0", + "node-fetch": "^3.3.2", + "openai": "^4.77.0", + "pdf-parse": "github:iamh2o/pdf-parse#1.1.3", + "pdf2pic": "^3.1.3", + "sanitize-filename": "^1.6.3", + "turndown": "^7.2.0", + "unist-util-visit": "^5.0.0" + } +} diff --git a/apps/converter/pdfToMarkdown.js b/apps/converter/pdfToMarkdown.js new file mode 100644 index 000000000..8508601d3 --- /dev/null +++ b/apps/converter/pdfToMarkdown.js @@ -0,0 +1,53 @@ +import pdf2md from '@opendocsg/pdf2md'; +import fs from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { generateFolderName, validateAndFixMarkdown } from './utils/utils.js'; + +// @todo Fix this to work locally and live +const isLagoon = !!process.env.LAGOON; +const __dirname = isLagoon + ? '/app/web/sites/default/files/converted' + : '/tmp/converted'; + +export async function pdfToMarkdown(pdfPath) { + try { + // Validate input file exists and is a PDF + if (!fs.existsSync(pdfPath) || !pdfPath.toLowerCase().endsWith('.pdf')) { + throw new Error('Invalid PDF file path'); + } + + // Generate output folder name + const folderName = generateFolderName(pdfPath); + const outputDir = path.join(__dirname, folderName); + const imagesDir = path.join(outputDir, 'images'); + + // Create output directories + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + if (!fs.existsSync(imagesDir)) { + fs.mkdirSync(imagesDir); + } + + const pdfBuffer = fs.readFileSync(pdfPath); + const fullMarkdown = await pdf2md(pdfBuffer); + + // const fullMarkdown = await convertToMarkdown(markdown); + const { markdown: fixedMarkdown, warnings } = + validateAndFixMarkdown(fullMarkdown); + + // Save markdown file + const mdPath = path.join(outputDir, 'content.md'); + await fs.writeFile(mdPath, fixedMarkdown); + + return { + markdownPath: mdPath, + warnings: warnings, // You could add warnings for failed image downloads etc. + outputDir, + }; + } catch (error) { + throw new Error(`PDF conversion failed: ${error.message}`); + } +} diff --git a/apps/converter/utils/utils.js b/apps/converter/utils/utils.js new file mode 100644 index 000000000..e9d2dac82 --- /dev/null +++ b/apps/converter/utils/utils.js @@ -0,0 +1,87 @@ +import crypto from 'crypto'; +import fs from 'fs-extra'; +import TurndownService from 'turndown'; + +export function validateAndFixMarkdown(markdown) { + const warnings = []; + + // Regex to match the entire image syntax + const imageRegex = /!\[.*?\]\(.*?\)/g; + + markdown = markdown.replace(imageRegex, (match) => { + // Parse the components of the Markdown image syntax + const altMatch = match.match(/!\[(.*?)\]/); // Match alt text + const urlMatch = match.match(/\((.*?)(?=\s|$)/); // Match URL + const titleMatch = match.match(/"([^"]*?)"\)$/); // Match title (if it exists) + + let altText = altMatch ? altMatch[1] : ''; + let url = urlMatch ? urlMatch[1] : ''; + let title = titleMatch ? titleMatch[1] : null; + + // Fix double quotes in alt text + if (altText.includes('"')) { + warnings.push(`Double quotes in alt text fixed: "${altText}"`); + altText = altText.replace(/"/g, "'"); + } + + // Fix double quotes in title + if (title && title.includes('"')) { + warnings.push(`Double quotes in title fixed: "${title}"`); + title = title.replace(/"/g, "'"); + } + + // Rebuild the image syntax + return title ? `![${altText}](${url} "${title}")` : `![${altText}](${url})`; + }); + + // Trim leading and trailing whitespace + const trimmedMarkdown = markdown.trim(); + if (markdown !== trimmedMarkdown) { + warnings.push('Leading or trailing whitespace detected and removed.'); + markdown = trimmedMarkdown; + } + + return { markdown, warnings }; +} + +export function generateFolderName(path) { + const hash = crypto.createHash('md5').update(path).digest('hex'); + return hash.substring(0, 12); +} + +export function convertToMarkdown(input) { + const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + hr: '---', + bulletListMarker: '-', + strongDelimiter: '**', + }); + + turndownService.addRule('tables', { + filter: 'table', + replacement: function (content, node) { + const rows = node.querySelectorAll('tr'); + const headers = Array.from(rows[0]?.querySelectorAll('th,td') || []) + .map((cell) => cell.textContent.trim()) + .join(' | '); + + const separator = headers + .split('|') + .map(() => '---') + .join(' | '); + + const body = Array.from(rows) + .slice(1) + .map((row) => + Array.from(row.querySelectorAll('td')) + .map((cell) => cell.textContent.trim()) + .join(' | '), + ) + .join('\n'); + + return `\n${headers}\n${separator}\n${body}\n\n`; + }, + }); + return turndownService.turndown(input); +} diff --git a/apps/converter/wordToMarkdown.js b/apps/converter/wordToMarkdown.js new file mode 100644 index 000000000..26d3ab4ea --- /dev/null +++ b/apps/converter/wordToMarkdown.js @@ -0,0 +1,65 @@ +import crypto from 'crypto'; +import fs from 'fs-extra'; +import imageType from 'image-type'; +import mammoth from 'mammoth'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { convertToMarkdown, generateFolderName } from './utils/utils.js'; + +// @todo Fix this to work locally and live +const isLagoon = !!process.env.LAGOON; +const __dirname = isLagoon + ? '/app/web/sites/default/files/converted' + : '/tmp/converted'; // Local only + +async function getImageExtension(buffer) { + const type = await imageType(buffer); + return type ? `.${type.ext}` : '.png'; +} + +export async function wordToMarkdown(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error('File does not exist: ' + filePath); + } + + const folderName = generateFolderName(filePath); + const outputDir = path.join(__dirname, folderName); + const imagesDir = path.join(outputDir, 'images'); + + await fs.ensureDir(outputDir); + await fs.ensureDir(imagesDir); + + const options = { + convertImage: mammoth.images.imgElement(async (image) => { + const imageBuffer = await image.read(); + const extension = await getImageExtension(imageBuffer); + const filename = `image-${crypto.randomBytes(4).toString('hex')}${extension}`; + const imagePath = path.join(imagesDir, filename); + + await fs.writeFile(imagePath, imageBuffer); + + return { + src: path.join('images', filename), + }; + }), + }; + + const result = await mammoth.convertToHtml({ path: filePath }, options); + + let markdown = convertToMarkdown(result.value); + + markdown = markdown + .replace(/\n\s*\n\s*\n/g, '\n\n') + .replace(/!\[\]\(/g, '![image](') + .trim(); + + const mdPath = path.join(outputDir, 'content.md'); + await fs.writeFile(mdPath, markdown); + + return { + markdownPath: mdPath, + warnings: result.messages, + outputDir, + }; +} diff --git a/docker-compose.yml b/docker-compose.yml index 663f65444..f28d7b166 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,6 +121,22 @@ services: lagoon.type: node lagoon.name: preview + convertmd: + build: + context: . + target: convertmd + dockerfile: .lagoon/Dockerfile + environment: + <<: *default-environment + LAGOON_LOCALDEV_URL: convertmd-${COMPOSE_PROJECT_NAME:-slbtemplate}.docker.amazee.io + networks: + - amazeeio-network + - default + labels: + lagoon.type: node-persistent + lagoon.persistent.name: nginx # mount the persistent storage of nginx into this container + lagoon.persistent: /app/web/sites/default/files/ + networks: amazeeio-network: external: true diff --git a/packages/drupal/silverback_ai/README.md b/packages/drupal/silverback_ai/README.md new file mode 100644 index 000000000..be933eb97 --- /dev/null +++ b/packages/drupal/silverback_ai/README.md @@ -0,0 +1,30 @@ +## INTRODUCTION + +The Silverback AI module is a base module + +## REQUIREMENTS + +- Media +- Webform (using some webform elements on reporting) + +## INSTALLATION + +Install as you would normally install a contributed Drupal module. +See: for further information. + +## CONFIGURATION + +- Open AI credentials can be set on: `/admin/config/system/silverback-ai-settings`. +It is recommended though to add the Open AI Api key as environment variable (`OPEN_AI_API_KEY`). + +## USAGE TRACKING + +The Silverback AI module tracks OpenAI API token usage for monitoring and cost management purposes: + +- All Silverback AI submodules automatically report their token usage through the `TokenUsage` service +- Usage statistics can be viewed at `/admin/reports/silverback-ai-usage` +- The report shows: + - Total tokens used per module + - Cost estimates based on current OpenAI pricing + - Usage breakdown by time period + - Details of individual API calls diff --git a/packages/drupal/silverback_ai/config/install/silverback_ai.settings.yml b/packages/drupal/silverback_ai/config/install/silverback_ai.settings.yml new file mode 100644 index 000000000..ed081fab3 --- /dev/null +++ b/packages/drupal/silverback_ai/config/install/silverback_ai.settings.yml @@ -0,0 +1,2 @@ +open_ai_base_uri: 'https://api.openai.com/v1/' +open_ai_api_key: '' diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/README.md b/packages/drupal/silverback_ai/modules/silverback_ai_import/README.md new file mode 100644 index 000000000..7075d9389 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/README.md @@ -0,0 +1,101 @@ +### Silverback AI Import Module + +--- + +## Introduction + +The **Silverback AI Import** module enables advanced content importation using AI-driven parsing and conversion. It provides tools to process content from sources such as Microsoft Word documents and HTML pages, transforming them into Gutenberg-compatible blocks for Drupal sites. + +This module is ideal for websites requiring seamless integration of structured content into their CMS, leveraging AI for efficiency and accuracy. + +--- + +## Features + +- **Content Import Support**: Import content from: + +  - Microsoft Word (`.docx`) files. + +  - HTML URLs. + +- **AI-driven Parsing**: Extract and process content using the OpenAI model. + +- **Custom Gutenberg Blocks**: Automatically convert content into pre-defined block types such as headers, lists, paragraphs, tables, and images. + +- **Batch Processing**: Import large content sets efficiently using batch operations. + +--- + +## Requirements + +The module depends on: + +- **Drupal Core**: Versions 10 or 11. + +- **Silverback AI** module. + +- **Media** module for handling images. + +Ensure these dependencies are installed before enabling the module. + +--- + +## Configuration + +1\. Navigate to the **AI Import Settings** page:   + +   `Admin > Configuration > System > Silverback Import AI Settings` + +2\. Set the following: + +   - **OpenAI Model**: Select or configure the OpenAI model. + +   - **Converter Service URL**: Provide the URL for the external service used to parse and process files. + +3\. Access the import functionality when creating or editing content. + +--- + +## Usage + +1\. **Importing Content**: + +   - Add a content node and locate the "Import Content" section. + +   - Choose a source: + +     - **Microsoft Word File**: Upload a `.docx` file. + +     - **HTML Page**: Enter a valid URL. + +   - Process the import to convert content into structured blocks. + +2\. **Batch Import**: + +   - Large sets of data can be processed using the batch handler available in the module. [TBD] + +--- + +## Custom Plugins + +The module supports extensible AI plugins for various content types: + +- **Default**: Generic HTML content. + +- **Header**: Markdown headers. + +- **Image**: Image embedding and metadata. + +- **List**: Ordered and unordered lists. + +- **Paragraph**: Text paragraphs. + +- **Table**: Tabular data. + +Developers can add custom plugins by extending the `AiImportPluginManagerInterface`. + +--- + +## Maintainers + +This module is maintained by the **Silverback** team. diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/config/schema/silverback_ai_import.schema.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/config/schema/silverback_ai_import.schema.yml new file mode 100644 index 000000000..b904f1b8a --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/config/schema/silverback_ai_import.schema.yml @@ -0,0 +1,8 @@ +# Schema for the configuration files of the silverback_ai_import module. +silverback_ai_import.settings: + type: config_object + label: 'silverback_ai_import settings' + mapping: + example: + type: string + label: 'Example' diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.info.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.info.yml new file mode 100644 index 000000000..9c154fcf0 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.info.yml @@ -0,0 +1,8 @@ +name: 'Silverback Import AI' +type: module +description: 'Silverback AI content import and more' +package: Silverback +core_version_requirement: ^10 || ^11 +dependencies: + - silverback_ai:silverback_ai + - media:media diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.install b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.install new file mode 100644 index 000000000..c0cef5037 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.install @@ -0,0 +1,86 @@ +schema(); + if ($db_schema->tableExists('silverback_ai_import')) { + $db_schema->dropTable('silverback_ai_import'); + } + + $schema['silverback_ai_import'] = [ + 'description' => 'Import log for the Silverback AI import module.', + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key.', + ], + 'uid' => [ + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user executed the ai fetch action.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'timestamp' => [ + 'description' => 'Date/time if the import, as Unix timestamp.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'target_entity_type_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => FALSE, + 'default' => '', + 'description' => 'The ID of the associated entity type.', + ], + 'target_entity_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The ID of the associated entity.', + ], + 'target_entity_revision_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The revision ID of the associated entity.', + ], + 'source' => [ + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'normal', + 'description' => 'The source of the import.', + ], + 'output_folder' => [ + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'small', + 'description' => 'The name of the folder exported.', + ], + 'data' => [ + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'description' => 'The import response *usually a text series of gutenberg formatted blocks.', + ], + 'primary key' => ['id'], + 'indexes' => [ + 'uid' => ['uid'], + 'timestamp' => ['timestamp'], + ], + ], + ]; + + return $schema; +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.action.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.action.yml new file mode 100644 index 000000000..3d9bf7ed2 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.action.yml @@ -0,0 +1,5 @@ +silverback_ai_ipmort.add_page: + route_name: silverback_ai_import.page_drop + title: 'Page Drop' + appears_on: + - system.admin_content diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.menu.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.menu.yml new file mode 100644 index 000000000..440c29f20 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.links.menu.yml @@ -0,0 +1,6 @@ +silverback_ai_import.import_ai_settings: + title: Import AI settings + description: Settings for content import (AI) + parent: silverback_ai.admin_config_ai + route_name: silverback_ai_import.import_ai_settings + weight: 11 diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.module b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.module new file mode 100644 index 000000000..1483185a9 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.module @@ -0,0 +1,186 @@ + 'details', + '#title' => t('Import content'), + '#open' => TRUE, + '#weight' => 99, + ]; + $form['import']['import_type'] = [ + '#type' => 'radios', + '#title' => t('Import content from:'), + '#options' => [ + 'none' => t('Do not import any content'), + 'docx' => t('Microsoft Word file'), + 'pdf' => t("PDF file"), + 'url' => t("Remote HTML page"), + ], + '#default_value' => 'none', + ]; + + $form['import']['container_docx'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'docx'], + ], + ], + ]; + + $form['import']['container_docx']['file'] = [ + '#title' => t('Drag and drop a Microsoft Word file'), + '#type' => 'dropzonejs', + '#required' => TRUE, + '#dropzone_description' => 'Drag and drop a file here', + '#max_filesize' => '20M', + '#max_files' => 1, + '#extensions' => 'doc docx', + '#upload_location' => 'public://converted/', + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'docx'], + ], + ], + ]; + + $form['import']['container_url'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'url'], + ], + ], + ]; + + $form['import']['container_url']['url_value'] = [ + '#type' => 'url', + '#title' => t('URL'), + '#maxlength' => 2048, + '#size' => 128, + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'url'], + ], + ], + ]; + + $form['import']['container_pdf'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'pdf'], + ], + ], + ]; + + $form['import']['container_pdf']['pdf_file'] = [ + '#title' => t('Drag and drop a PDF file'), + '#type' => 'dropzonejs', + '#required' => TRUE, + '#dropzone_description' => 'Drag and drop a file here', + '#max_filesize' => '24M', + '#max_files' => 1, + '#extensions' => 'pdf', + '#upload_location' => 'public://converted/', + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'pdf'], + ], + ], + ]; + + $form['actions']['submit']['#submit'][] = '_silverback_ai_import_form_submit'; + $form['#validate'][] = '_silverback_ai_import_form_submit_validate'; + // Better to have this unpublished originally, and then + // we will display a message to the user (esp. if there is AI content) + $form['moderation_state']['#access'] = FALSE; + $form['actions']['submit']['#value'] = t('Create'); +} + +/** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return void + */ +function _silverback_ai_import_form_submit_validate(array $form, FormStateInterface $form_state) { + // @todo + $type = $form_state->getValue('import_type'); + if ($type == ContentImportAiService::PDF) { + $file = $form_state->getValue('pdf_file'); + if (empty($file['uploaded_files'])) { + $form_state->setErrorByName('pdf_file', t('PDF file is required.')); + } + } + + if ($type == ContentImportAiService::DOCX) { + $file = $form_state->getValue('file'); + if (empty($file['uploaded_files'])) { + $form_state->setErrorByName('file', t('DOCX file is required.')); + } + } +} + +/** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return void + */ +function _silverback_ai_import_form_submit(array $form, FormStateInterface $form_state) { + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $form_state->getFormObject()->getEntity(); + $url_value = $form_state->getValue('url_value'); + + $service = \Drupal::service('silverback_ai_import.content'); + $content = \Drupal::service('silverback_ai_import.batch.import'); + $type = $form_state->getValue('import_type'); + $contentImportLogger = \Drupal::service('silverback_ai_imoprt.logger'); + + switch ($type) { + case ContentImportAiService::PDF: + $file = $form_state->getValue('pdf_file'); + break; + case ContentImportAiService::URL: + $file = $url_value; + break; + case ContentImportAiService::NONE: + break; + default: + $file = $form_state->getValue('file'); + } + + if (in_array($type, [ContentImportAiService::DOCX, ContentImportAiService::PDF])) { + $file = $service->createFileEntityFromDropzoneData($file); + } + + if (!empty($file)) { + $ast = $service->getAst($file, $type); + $flatten = $service->flattenAst($ast->content); + $content->create($flatten, $entity); + $contentImportLogger->createEntry($ast, $entity, $file->getFileUri()); + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.routing.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.routing.yml new file mode 100644 index 000000000..6e46cb547 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.routing.yml @@ -0,0 +1,15 @@ +silverback_ai_import.import_ai_settings: + path: '/admin/config/system/silverback/import-ai-settings' + defaults: + _title: 'Import AI settings' + _form: 'Drupal\silverback_ai_import\Form\ImportAiSettingsForm' + requirements: + _permission: 'administer site configuration' + +silverback_ai_import.page_drop: + path: '/admin/config/content/silverback-page-drop' + defaults: + _title: 'Silverback Page Drop' + _form: 'Drupal\silverback_ai_import\Form\PageDropForm' + requirements: + _permission: 'access content' diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.services.yml b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.services.yml new file mode 100644 index 000000000..1a82f6e76 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/silverback_ai_import.services.yml @@ -0,0 +1,17 @@ +services: + silverback_ai_import.content: + class: Drupal\silverback_ai_import\ContentImportAiService + arguments: ['@current_route_match', '@current_user', '@entity_type.manager', '@logger.factory', '@config.factory', '@silverback_ai.openai_http_client', '@plugin.manager.ai.import', '@plugin.manager.ai.post.import', '@silverback_ai.service', '@silverback_ai.token.usage'] + silverback_ai_import.batch.import: + class: 'Drupal\silverback_ai_import\ContentImportBatch' + arguments: + - '@logger.factory' + plugin.manager.ai.import: + class: Drupal\silverback_ai_import\AiImportPluginManager + arguments: ['@container.namespaces', '@cache.default', '@module_handler'] + plugin.manager.ai.post.import: + class: Drupal\silverback_ai_import\AiPostImportPluginManager + arguments: ['@container.namespaces', '@cache.default', '@module_handler'] + silverback_ai_imoprt.logger: + class: Drupal\silverback_ai_import\ContentImportLoggerService + arguments: ['@database', '@current_user', '@logger.factory', '@config.factory', '@entity_type.manager'] diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManager.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManager.php new file mode 100644 index 000000000..983532c3f --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManager.php @@ -0,0 +1,34 @@ +alterInfo('ai_import_info'); + $this->setCacheBackend($cache_backend, 'ai_import_info'); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManagerInterface.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManagerInterface.php new file mode 100644 index 000000000..1776daa06 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiImportPluginManagerInterface.php @@ -0,0 +1,62 @@ +alterInfo('ai_post_import_info'); + $this->setCacheBackend($cache_backend, 'ai_post_import_info'); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiPostImportPluginManagerInterface.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiPostImportPluginManagerInterface.php new file mode 100644 index 000000000..7104c6e29 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/AiPostImportPluginManagerInterface.php @@ -0,0 +1,18 @@ +getPlugin($chunk); + return $plugin->convert($chunk); + } + + /** + * Get Abstract Syntax Tree (AST) from different source types + * + * @param FileInterface|string $source The source to parse (can be a FileInterface object or string path/URL) + * @param int $type The type of source (self::DOCX by default, self::URL, or self::PDF) + * @return mixed The Abstract Syntax Tree representation of the source + * + * @throws \Exception If the source is invalid or cannot be parsed + */ + public function getAst(FileInterface|string $source, $type = self::DOCX) { + $handlers = [ + self::DOCX => 'getAstFromFilePath', + self::URL => 'getAstFromUrl', + self::PDF => 'getAstFromPdfFile', + ]; + $handler = $handlers[$type]; + return $this->$handler($source); + } + + /** + * Retrieves the AST (Abstract Syntax Tree) from a given file path using an HTTP service. + * + * @param \Drupal\file\FileInterface $file + * The file for which to generate the AST. + * + * @return mixed + * The decoded JSON response containing the AST from the external service, or NULL if the request fails. + * + * @throws \GuzzleHttp\Exception\RequestException|\GuzzleHttp\Exception\GuzzleException + * Thrown when the HTTP request fails, though it is caught and logged within this method. + * + * @todo Implement configuration handling for service endpoints or client headers. + */ + private function getAstFromFilePath(FileInterface $file) { + $uri = $file->getFileUri(); + $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')->getViaUri($uri); + $file_path = $stream_wrapper_manager->realpath(); + $parse_service_url = $this->configFactory->get('silverback_ai_import.settings')->get('converter_service_url'); + // @todo Add DI. + $client = \Drupal::httpClient(); + try { + // @todo For now this is working only for docx files. + $response = $client->request('GET', "{$parse_service_url}/convert?path={$file_path}", [ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + $body = $response->getBody()->getContents(); + $response = json_decode($body); + } catch (RequestException $e) { + // Handle any errors. + $this->loggerFactory->get('silverback_ai_import')->error($e->getMessage()); + } + return $response; + } + + /** + * Retrieves an Abstract Syntax Tree (AST) from a URL using an external conversion service. + * + * Currently only supports DOCX files. Sends the URL to a configured HTML conversion + * service and returns the AST representation of the document. + * + * @param string $url + * The URL of the document to convert (currently only DOCX files) + * + * @return object|null + * Returns the decoded JSON response containing the AST if successful, + * or NULL if the request fails + * + * @throws \GuzzleHttp\Exception\GuzzleException + * When the HTTP request fails + * When JSON decoding fails + * + * @todo Extend support for other file types besides DOCX + * + * @see \GuzzleHttp\ClientInterface::request() + */ + private function getAstFromUrl(string $url) { + $parse_service_url = $this->configFactory->get('silverback_ai_import.settings')->get('converter_service_url'); + $client = \Drupal::httpClient(); + try { + // @todo For now this is working only for docx files. + $response = $client->request('GET', "{$parse_service_url}/html-convert?path={$url}", [ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + $body = $response->getBody()->getContents(); + return json_decode($body); + } catch (RequestException $e) { + // Handle any errors. + $this->loggerFactory->get('silverback_ai_import')->error($e->getMessage()); + } + + return NULL; + } + + /** + * Converts a PDF file to an Abstract Syntax Tree (AST) using an external service. + * + * Takes a Drupal file entity containing a PDF, resolves its real path, and sends it + * to a configured conversion service. The service returns a JSON response containing + * the PDF's AST representation. + * + * @param \Drupal\file\FileInterface $file + * The PDF file entity to convert. + * + * @return object|null + * Returns the decoded JSON response containing the AST if successful, + * or NULL if the request fails + * + * @throws \GuzzleHttp\Exception\GuzzleException + * When the HTTP request fails + * @throws \RuntimeException + * When stream wrapper manager fails to resolve the file path + * + * @see \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::getViaUri() + * @see \GuzzleHttp\ClientInterface::request() + */ + private function getAstFromPdfFile(FileInterface $file) { + $uri = $file->getFileUri(); + $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')->getViaUri($uri); + $file_path = $stream_wrapper_manager->realpath(); + $parse_service_url = $this->configFactory->get('silverback_ai_import.settings')->get('converter_service_url'); + + $client = \Drupal::httpClient(); + try { + $response = $client->request('GET', "{$parse_service_url}/pdf-convert?path={$file_path}", [ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + $body = $response->getBody()->getContents(); + $response = json_decode($body); + } catch (RequestException $e) { + // Handle any errors. + $this->loggerFactory->get('silverback_ai_import')->error($e->getMessage()); + } + return $response; + } + + /** + * Helper method. + * + * @param string $ast + * @param string $schema + * + * @return array + * @throws \Exception + */ + public function extractData(string $ast, string $schema) { + $model = $this->configFactory->get('silverback_ai_import.settings')->get('ai_model') ?: self::DEFAULT_AI_MODEL; + + $prompt = <<ai->request($prompt, $model, ['module' => 'silverback_import_ai']); + } + + /** + * Send the AI request. + * + * @param string $ast + * @param string $type + * @param string $template + * @param string $schema + * + * @return array + * @throws \Exception + */ + public function sendOpenAiRequest(string $ast, string $type, string $template, string $schema) { + + $model = $this->configFactory->get('silverback_ai_import.settings')->get('ai_model') ?: self::DEFAULT_AI_MODEL; + + $prompt = <<ai->request($prompt, $model, ['module' => 'silverback_import_ai']); + } + + /** + * Extract data from markdown using AI. + * + * @param string $markdown + * + * @return array + * @throws \Exception + */ + public function extractBaseDataFromMarkdown(string $markdown) { + + $model = $this->configFactory->get('silverback_ai_import.settings')->get('ai_model') ?: self::DEFAULT_AI_MODEL; + + $prompt = <<ai->request($prompt, $model, ['module' => 'silverback_import_ai']); + } + + /** + * Retrieves a plugin instance that matches the specified chunk. + * + * This method creates an instance of the default AI plugin and then + * iterates through all available plugin definitions to find a plugin + * that matches the provided chunk. The first matching plugin instance + * will be selected and returned. + * + * @param array $chunk + * The input data that will be used to match against plugin definitions. + * + * @return object + * The plugin instance that matches the provided chunk or the default + * plugin if no matches are found. + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @todo Order the plugin definitions by weight before attempting to find a match. + * + */ + public function getPlugin(array $chunk) { + $default_plugin = $this->pluginManager->createInstance('ai_default'); + $definitions = $this->pluginManager->getDefinitions(); + // @todo Order by weight. + foreach ($definitions as $definition) { + $plugin = $this->pluginManager->createInstance($definition['id'], ['chunk' => $chunk]); + if ($plugin->matches($chunk)) { + $default_plugin = $plugin; + break; + } + } + return $default_plugin; + } + + /** + * Flattens a hierarchical AST (Abstract Syntax Tree) into a linear array of nodes. + * + * This function converts a nested AST structure into a flat array where each node + * is assigned a unique ID and maintains a reference to its parent. It processes + * specific node types differently and handles recursive traversal of child nodes. + * + * @param $ast + * The AST structure to flatten. + * @param int|null $parent + * The ID of the parent node (used in recursion) + * + * @return array An array of flattened nodes, where each node contains: + * - type: The capitalized node type + * - id: A unique identifier + * - parent: Reference to the parent node's ID + * - Additional properties specific to each node type + */ + public function flattenAst($ast, int $parent = NULL) { + + $ast = json_decode(json_encode($ast), TRUE); + static $flatNodes = []; + static $id; + + if ($ast === NULL) { + return $flatNodes; + } + + foreach ($ast as $chunk) { + if ( + isset($chunk['type']) + && in_array($chunk['type'], [ + 'Strong', + 'Text', + 'ListItem', + 'Emphasis', + ]) + ) { + continue; + } + + if ( + isset($chunk['type']) + && $chunk['type'] == 'Link' + && isset($chunk['children']) + && count($chunk['children']) == 1 + && $chunk['children'][0]['type'] !== 'Image' + ) { + continue; + } + + $children = $chunk['children'] ?? []; + // Chunk preprocessing. + $chunk['type'] = ucfirst($chunk['type']); + $chunk['id'] = ++$id; + $chunk['parent'] = $parent; + + $flatNodes[] = $chunk; + // Recursively process children. + foreach ($children as $child) { + $this->flattenAst([$child], $id); + } + } + + return $flatNodes; + } + + /** + * Recursively iterates through a nested array to process Image type items. + * + * Traverses through the array and its nested children, processing any items + * of type 'Image' by adding a 'gutenberg' property. The function modifies + * the array in place using reference parameters. + * + * @param array &$data + * The array to process, passed by reference + * Expected structure: [ + * 'type' => string, + * 'children' => array (optional) + * ]. + * @param int $depth + * Current depth in the recursive traversal (default: 0) +Ω * + * @throws \JsonException + * @see processChunk() Method used to process Image type items + */ + public function iterateArray(array &$data, int $depth = 0): void { + foreach ($data as &$item) { + // Process item here. + if (isset($item['type'])) { + if ($item['type'] == 'Image') { + $item['gutenberg'] = $this->processChunk($item); + } + } + if (isset($item['children']) && is_array($item['children'])) { + $this->iterateArray($item['children'], $depth + 1); + } + } + } + + /** + * Extracts various metadata from a given URL by fetching and parsing its HTML content. + * + * This function attempts to retrieve the HTML content of a URL and extract key information + * including title, path, meta tags, and language settings. It includes error handling for + * various failure scenarios. + * + * @param string $url + * The URL to extract data from. + * + * @return array An associative array containing: + * - title: string|null The page title if found + * - path: string The URL path component, defaults to "/" if not found + * - metatags: array Meta tag name-content pairs + * - language: string|null The page language if specified + * - error: string|null Error message if any error occurred, null otherwise + * + * @throws \Exception Caught internally and returned as error in result array + */ + public function extractPageDataFromUrl($url) { + $data = [ + 'title' => NULL, + 'path' => NULL, + 'metatags' => [], + 'language' => NULL, + 'error' => NULL, + ]; + + // Validate URL. + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $data['error'] = "Invalid URL"; + return $data; + } + + try { + // Use file_get_contents with a user agent to avoid being blocked by some servers. + $options = [ + 'http' => [ + 'method' => 'GET', + // Example user agent. + 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + // Timeout in seconds. + 'timeout' => 10, + ], + ]; + $context = stream_context_create($options); + $html = @file_get_contents($url, FALSE, $context); + + if ($html === FALSE) { + $error = error_get_last(); + $data['error'] = "Failed to fetch URL: " . ($error ? $error['message'] : "Unknown error"); + return $data; + } + + if (preg_match('/(.*?)<\/title>/i', $html, $matches)) { + $data['title'] = trim(html_entity_decode($matches[1])); + } + + $data['path'] = parse_url($url, PHP_URL_PATH); + if ($data['path'] === NULL) { + // Handle cases where there's no path. + $data['path'] = "/"; + } + + // Extract Meta Tags. + preg_match_all('/<meta\s+(?:name|http-equiv)="([^"]*)"\s+content="([^"]*)"/i', $html, $matches); + for ($i = 0; $i < count($matches[0]); $i++) { + $name = strtolower($matches[1][$i]); + $data['metatags'][$name] = trim(html_entity_decode($matches[2][$i])); + } + + // Extract Language. + if (preg_match('/<html.*?lang="([^"]*)"/i', $html, $matches)) { + $data['language'] = $matches[1]; + } elseif (preg_match('/<meta\s+http-equiv="Content-Language"\s+content="([^"]*)"/i', $html, $matches)) { + $data['language'] = $matches[1]; + } + } catch (\Exception $e) { + $data['error'] = "An error occurred: " . $e->getMessage(); + } + + return $data; + } + + /** + * Creates a Node entity from a DOCX Abstract Syntax Tree (AST). + * + * Processes the content.md file from the AST's output directory, + * extracts data through markdown processing, and creates a new node + * entity if valid data is present in the expected JSON structure. + * + * @param object $ast + * The AST object containing outputDirectory property + * with path to the processed DOCX content. + * + * @return \Drupal\node\Entity\Node|null + * Returns a Node entity if creation is successful and data is valid, + * or NULL if required data structure is not found + * + * @throws \Drupal\Core\Entity\EntityStorageException + * When there's an error saving the node entity + * @throws \RuntimeException + * When the content.md file cannot be read + * + * @see \Drupal\node\Entity\Node::create() + */ + public function createEntityFromDocxAst($ast) { + $markdown = file_get_contents($ast->outputDirectory . '/content.md'); + $data = $this->extractBaseDataFromMarkdown($markdown); + // @todo Surround with try-catch + if (isset($data['choices'][0]['message']['content'])) { + $data = json_decode($data['choices'][0]['message']['content'], TRUE); + $entity = Node::create([ + 'type' => 'page', + 'title' => $data['title'], + 'langcode' => strtolower($data['language']), + ]); + $entity->save(); + return $entity; + } + return NULL; + } + + /** + * Creates a Node entity of type 'page' from a given URL. + * + * Extracts page data from the provided URL and creates a new node entity + * if the required data (title and language) is available. + * + * @param string $url + * The URL to extract page data from. + * + * @return \Drupal\node\Entity\Node|null + * Returns a Node entity if creation is successful, + * or NULL if required data is missing + * + * @throws \Drupal\Core\Entity\EntityStorageException + * @throws \Exception + * When there's an error saving the node entity + * + * @see \Drupal\node\Entity\Node::create() + */ + public function createEntityFromUrl($url) { + $data = $this->extractPageDataFromUrl($url); + // @todo Handle exceptions + if (!empty($data['title']) && !empty($data['language'])) { + $entity = Node::create([ + 'type' => 'page', + 'title' => $data['title'], + 'langcode' => strtolower($data['language']), + ]); + $entity->save(); + return $entity; + } + return NULL; + } + + /** + * Creates a File entity from Dropzone uploaded file data. + * + * This function handles the process of copying an uploaded file to a designated + * public directory and creating a corresponding File entity in Drupal. + * + * @param array $file_data + * The file data from Dropzone upload + * Expected structure: ['uploaded_files'][0]['path']. + * + * @return \Drupal\file\Entity\File The created and saved file entity + * + * @throws \Drupal\Core\File\Exception\FileException When file operations fail + * @throws \Drupal\Core\Entity\EntityStorageException When file entity creation fails + * + * @see \Drupal\Core\File\FileSystemInterface::prepareDirectory() + * @see \Drupal\Core\File\FileSystemInterface::copy() + * @see \Drupal\file\Entity\File::create() + */ + public function createFileEntityFromDropzoneData($file_data) { + // @todo Handle exceptions + $filepath = $file_data['uploaded_files'][0]['path']; + $directory = 'public://converted'; + $file_system = \Drupal::service('file_system'); + $file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + $file_system->copy($filepath, $directory . '/' . basename($filepath), FileSystemInterface::EXISTS_REPLACE); + + $file = File::create([ + 'filename' => basename($filepath), + 'uri' => "{$directory}/" . basename($filepath), + 'status' => FileInterface::STATUS_PERMANENT, + 'uid' => $this->currentUser->id() ?? self::ADMINISTRATOR_ID, + ]); + $file->setPermanent(); + $file->save(); + return $file; + } + + /** + * Get all post import plugins. + * + * @return array + */ + public function getPostImportPlugins() { + $definitions = $this->pluginManagerPost->getDefinitions(); + $plugins = []; + foreach ($definitions as $definition) { + $plugins[] = $definition['id']; + } + return $plugins; + } + + /** + * Post import process for plugin. + * + * @param $plugin_id + * @param $chunks + * + * @return mixed + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function postProcessChunks($plugin_id, $chunks,) { + $plugin = $this->pluginManagerPost->createInstance($plugin_id); + return $plugin->convert($chunks); + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportBatch.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportBatch.php new file mode 100644 index 000000000..28c62fed4 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportBatch.php @@ -0,0 +1,197 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai_import; + +use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Logger\LoggerChannelInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\node\Entity\Node; + +/** + * Methods for running the ConfigImporter in a batch. + * + * @see \Drupal\Core\Config\ConfigImporter + */ +class ContentImportBatch { + use StringTranslationTrait; + + /** + * The logger channel. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected LoggerChannelInterface $loggerChannel; + + /** + * Constructor. + * + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory + * The logger factory. + */ + public function __construct(LoggerChannelFactoryInterface $loggerFactory) { + $this->loggerChannel = $loggerFactory->get('silverback_image_ai'); + } + + /** + * Creates a batch operation to process media image updates. + * + * This method initializes a batch process for updating media images, setting + * up the batch operations and conditions for Drush integration if run via CLI. + * + * @return void + */ + public function create(array $chunks, $entity): void { + + $batchBuilder = (new BatchBuilder()) + ->setTitle($this->t('Running import...')) + ->setFinishCallback([self::class, 'finish']) + ->setInitMessage('The initialization message (optional)') + ->setProgressMessage('Completed @current of @total.'); + + $total = count($chunks); + $count = 0; + // Create multiple batch operations based on the $batchSize. + foreach ($chunks as $chunk) { + $item = [ + 'chunk' => $chunk, + 'nid' => $entity->id(), + ]; + $batch = [ + 'item' => $item, + 'count' => $count++, + 'total' => $total, + ]; + $batchBuilder->addOperation([ContentImportBatch::class, 'process'], [$batch]); + } + + // @todo Add DI + $service = \Drupal::service('silverback_ai_import.content'); + $post_import_plugins = $service->getPostImportPlugins(); + foreach ($post_import_plugins as $plugin) { + $batch = [ + 'plugin_id' => $plugin, + 'entity' => $entity, + 'count' => $count++, + 'total' => $total, + ]; + $batchBuilder->addOperation([ContentImportBatch::class, 'postProcess'], [$batch]); + } + + // @todo Here, discover all post import plugins and add an operation at the end of this array. + batch_set($batchBuilder->toArray()); + } + + /** + * Batch operation callback. + * + * @param array $batch + * Information about batch (items, size, total, ...). + * @param array $context + * Batch context. + */ + public static function process(array $batch, array &$context) { + $processed = !empty($context['results']) ? count($context['results']) : $batch['count']; + $service = \Drupal::service('silverback_ai_import.content'); + $content = $service->processChunk($batch['item']['chunk']); + \Drupal::logger('silverback_ai_import')->debug($content); + $context['results']['content'][] = $content; + + $context['results']['nid'] = $batch['item']['nid']; + + $context['message'] = t('Processing chunk @processed/@total', [ + '@processed' => $processed, + '@total' => $batch['total'], + ]); + } + + /** + * Batch operation callback. + * + * @param array $batch + * Information about batch (items, size, total, ...). + * @param array $context + * Batch context. + */ + public static function postProcess(array $batch, array &$context) { + $service = \Drupal::service('silverback_ai_import.content'); + $processed_chunks = $service->postProcessChunks($batch['plugin_id'], $context['results']['content']); + $context['results']['content'] = $processed_chunks; + $processed = !empty($context['results']) ? count($context['results']) : $batch['count']; + $context['message'] = t('Processing chunk @processed/@total', [ + '@processed' => $processed, + '@total' => $batch['total'], + ]); + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + * + * @param bool $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of the operations that had not been completed by the batch API. + */ + public static function finish(bool $success, array $results, array $operations) { + $nid = $results['nid']; + if (!empty($nid)) { + $node = Node::load($nid); + + // @todo Possible we need more process here. + $results['content'] = array_map(function ($item) { + return str_replace('<p></p>', '', $item); + }, $results['content']); + + // @todo Improve that to respect also templates + $implode = implode(PHP_EOL, $results['content']); + $content = <<<EOD + + <!-- wp:custom/content --> + $implode + <!-- /wp:custom/content --> + + EOD; + + // @todo Add post import process here + try { + + // @todo + $config = \Drupal::service('config.factory')->get('gutenberg.settings'); + $node_type = $node->type->getString(); + $gutenberg_template = $config->get($node_type . '_template') ? json_decode($config->get($node_type . '_template')) : NULL; + $init = ''; + // @todo We can do better than this + foreach ($gutenberg_template as $item) { + $init .= "<!-- wp:{$item[0]} /-->\n"; + } + + $cleaned = str_replace('<!-- wp:custom/content /-->', $content, $init); + $node->body->value = $cleaned; + $node->save(); + } catch (\Exception $e) { + // @todo + } + } + + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Items processed successfully.')); + } else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = t( + 'An error occurred while processing %error_operation with arguments: @arguments', + ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)] + ); + $messenger->addError($message); + } + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportLoggerService.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportLoggerService.php new file mode 100644 index 000000000..8cde5cca9 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/ContentImportLoggerService.php @@ -0,0 +1,183 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai_import; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Link; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\file\FileInterface; +use Drupal\user\Entity\User; + +/** + * @todo Add class description. + */ +final class ContentImportLoggerService { + + private const USER_ADMIN = 1; + private const PAGER_LIMIT = 25; + + use StringTranslationTrait; + + /** + * Constructs a TokenUsage object. + */ + public function __construct( + private readonly Connection $connection, + private readonly AccountProxyInterface $currentUser, + private readonly LoggerChannelFactoryInterface $loggerFactory, + private readonly ConfigFactoryInterface $configFactory, + private readonly EntityTypeManagerInterface $entityTypeManager, + ) { + } + + /** + * @param $ast + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param string $source + * + * @return void + */ + public function createEntry($ast, EntityInterface $entity, string $source): void { + + $uid = self::USER_ADMIN; + if ($this->currentUser) { + $uid = $this->currentUser->id(); + } + + // @todo Validate input array + try { + $this->connection + ->insert('silverback_ai_import') + ->fields([ + 'uid' => $uid, + 'timestamp' => (new DrupalDateTime())->getTimestamp(), + 'target_entity_type_id' => $entity->getEntityTypeId(), + 'target_entity_id' => $entity->id(), + 'target_entity_revision_id' => NULL, + 'source' => $source ?? '', + 'output_folder' => $ast->outputDirectory, + 'data' => serialize($ast), + ]) + ->execute(); + } catch (\Exception $e) { + $this->loggerFactory->get('silverback_ai')->error($e->getMessage()); + } + } + + /** + * Retrieves a list of entries from the 'silverback_ai_import' table. + * + * This function queries the database to select fields related to AI usage + * and orders them by ID in descending order. It paginates the result + * according to a predefined limit. Each row fetched from the database + * is processed using the `buildRow` method before being added to the result set. + * + * @return array + * An array of processed database records. + */ + public function getEntries() { + $query = $this->connection->select('silverback_ai_import', 's') + ->fields('s', [ + 'id', + 'uid', + 'timestamp', + 'target_entity_id', + 'target_entity_type_id', + 'target_entity_revision_id', + 'source', + 'output_folder', + 'data', + ]) + ->orderBy('id', 'DESC'); + $pager = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit(self::PAGER_LIMIT); + $rsc = $pager->execute(); + $rows = []; + + foreach ($rsc->fetchAll() as $row) { + $rows[] = $this->buildRow($row); + } + + return $rows; + } + + /** + * Builds a renderable array representing a row of data. + * + * This method constructs an array of information based on the data from + * the provided row, including entity details, user information, and additional + * metadata such as timestamps and provider information. + * + * @param object $row + * The data row object containing properties such as 'target_entity_id', + * 'target_entity_type_id', 'uid', 'timestamp', 'total_count', 'provider', + * 'model', and 'module'. + * + * @return array + * Array with the following elements: + * - 'timestamp': The formatted timestamp of when the entry was created. + * - 'username': The display name of the user associated with the entry. + * - 'entity_id': The capitalized entity bundle string or empty string if + * the entity is not found. + * - 'info': A renderable link to detailed usage information displayed in + * a modal dialog. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function buildRow($row) { + $entity_info = ''; + if ($row->target_entity_id && $row->target_entity_type_id) { + // @todo Aldo check revision + $entity = $this->entityTypeManager->getStorage($row->target_entity_type_id)->load($row->target_entity_id); + $entity_info = $entity ? $entity->bundle() : ''; + // @todo Add url to entity. Problem is the e.g. File entities + // they return exception calling this method. + } + + $user = User::load($row->uid); + $username = ''; + if ($user) { + $username = $user->getDisplayName(); + } + + $icon_info = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> + <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/> + </svg>'; + + $link = Link::createFromRoute( + Markup::create($icon_info), + 'silverback_ai.ai_usage.details', + ['record' => $row->id], + [ + 'attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 800, + ]), + ], + 'attached' => [ + 'library' => ['core/drupal.dialog.ajax'], + ], + ] + ); + + return [ + 'timestamp' => DrupalDateTime::createFromTimestamp($row->timestamp)->format('d.m.Y H:i'), + 'username' => $username, + 'entity_id' => ucfirst($entity_info), + 'info' => $link, + ]; + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/ImportAiSettingsForm.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/ImportAiSettingsForm.php new file mode 100644 index 000000000..72280ffc9 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/ImportAiSettingsForm.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai_import\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Configure silverback_ai_import settings for this site. + */ +final class ImportAiSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'silverback_ai_import_import_ai_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['silverback_ai_import.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + $form['credentials'] = [ + '#type' => 'details', + '#title' => $this->t('Open AI model'), + '#open' => TRUE, + ]; + + // @todo Make this dynamically + $form['credentials']['ai_model'] = [ + '#type' => 'select', + '#title' => $this->t('Model'), + '#options' => [ + 'gpt-4o-mini' => 'gpt-4o-mini', + 'gpt-4o-mini-2024-07-18' => 'gpt-4o-mini-2024-07-18', + ], + '#empty_option' => $this->t('- Select model -'), + '#description' => $this->t('Leave empty to use the default <strong><em>gpt-4o-mini</em></strong> model.') . '<br />' . + $this->t('<strong><a href="@href" target="_blank">Learn more</a></strong> about the models.', [ + '@href' => 'https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence', + ]), + ]; + + $form['general'] = [ + '#type' => 'details', + '#title' => $this->t('General settings'), + '#open' => TRUE, + ]; + + $form['general']['converter_service_url'] = [ + '#type' => 'textfield', + '#title' => $this->t('Converter URL service'), + '#default_value' => $this->config('silverback_ai_import.settings')->get('converter_service_url'), + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('silverback_ai_import.settings') + ->set('converter_service_url', $form_state->getValue('converter_service_url')) + ->set('ai_model', $form_state->getValue('ai_model')) + ->save(); + parent::submitForm($form, $form_state); + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/PageDropForm.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/PageDropForm.php new file mode 100644 index 000000000..c55d1771f --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Form/PageDropForm.php @@ -0,0 +1,248 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai_import\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\silverback_ai_import\ContentImportAiService; + +/** + * Provides a Silverback Import AI form. + */ +final class PageDropForm extends FormBase { + + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'silverback_ai_import_page_drop'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + // End debug + // ---------------------. + $form['message'] = [ + '#type' => 'item', + '#markup' => Markup::create('<em>Create content by importing either a file or an existing URL.</em>'), + ]; + + $form['import'] = [ + '#type' => 'details', + '#title' => $this->t('Create content'), + '#open' => TRUE, + '#weight' => 99, + ]; + $form['import']['import_type'] = [ + '#type' => 'radios', + '#title' => $this->t('Create content from:'), + '#description' => $this->t('<em>* Experimental, use with caution</em>'), + '#options' => [ + 'docx' => $this->t('Microsoft Word file'), + 'pdf' => $this->t("PDF file (*)"), + 'url' => $this->t("Remote web page (*)"), + ], + '#default_value' => 'none', + '#required' => TRUE, + ]; + + $form['import']['container_docx'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'docx'], + ], + ], + ]; + + $form['import']['container_docx']['file'] = [ + '#title' => $this->t('Drag and drop a Microsoft Word file'), + '#type' => 'dropzonejs', + '#required' => TRUE, + '#dropzone_description' => 'Drag and drop a file here', + '#max_filesize' => '1M', + '#max_files' => 1, + '#extensions' => 'doc docx', + '#upload_location' => 'public://converted/', + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'docx'], + ], + ], + ]; + + $form['import']['container_url'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'url'], + ], + ], + ]; + + $form['import']['container_url']['url_value'] = [ + '#type' => 'url', + '#title' => $this->t('URL'), + '#maxlength' => 2048, + '#size' => 128, + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'url'], + ], + ], + ]; + + $form['import']['container_pdf'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'pdf'], + ], + ], + ]; + + $form['import']['container_pdf']['pdf_file'] = [ + '#title' => $this->t('Drag and drop a PDF file'), + '#type' => 'dropzonejs', + '#required' => TRUE, + '#dropzone_description' => 'Drag and drop a file here', + '#max_filesize' => '24M', + '#max_files' => 1, + '#extensions' => 'pdf', + '#upload_location' => 'public://converted/', + '#states' => [ + 'required' => [ + 'input[name="import_type"]' => ['value' => 'pdf'], + ], + ], + ]; + + $form['import']['container_output'] = [ + '#type' => 'container', + ]; + $form['import']['container_output']['output'] = [ + '#type' => 'item', + '#prefix' => '<div id="edit-output">', + '#suffix' => '</div>', + ]; + + $form['actions'] = [ + '#type' => 'actions', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'docx'], + ], + ], + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Import document'), + ], + ]; + + $form['actions_url'] = [ + '#type' => 'actions', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'url'], + ], + ], + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Import web page'), + ], + ]; + + $form['actions_pdf'] = [ + '#type' => 'actions', + '#states' => [ + 'visible' => [ + 'input[name="import_type"]' => ['value' => 'pdf'], + ], + ], + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Import PDF'), + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + $file = $form_state->getValue('file'); + $pdf_file = $form_state->getValue('pdf_file'); + + $type = $form_state->getValue('import_type'); + if ($type == 'docx' && empty($file['uploaded_files'])) { + $form_state->setErrorByName('file', $this->t('Please upload a file to import.')); + } + if ($type == 'pdf' && empty($pdf_file['uploaded_files'])) { + $form_state->setErrorByName('file', $this->t('Please upload a PDF to import.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + + $url_value = $form_state->getValue('url_value'); + + $service = \Drupal::service('silverback_ai_import.content'); + $content = \Drupal::service('silverback_ai_import.batch.import'); + $type = $form_state->getValue('import_type'); + $contentImportLogger = \Drupal::service('silverback_ai_imoprt.logger'); + + switch ($type) { + case ContentImportAiService::PDF: + $file = $form_state->getValue('pdf_file'); + break; + case ContentImportAiService::URL: + $file = $url_value; + break; + case ContentImportAiService::NONE: + break; + default: + $file = $form_state->getValue('file'); + } + + if (in_array($type, [ContentImportAiService::DOCX, ContentImportAiService::PDF])) { + $file = $service->createFileEntityFromDropzoneData($file); + } + + if (!empty($file)) { + $ast = $service->getAst($file, $type); + $entity = NULL; + + switch ($type) { + case ContentImportAiService::PDF: + $entity = $service->createEntityFromDocxAst($ast); + break; + case ContentImportAiService::URL: + $entity = $service->createEntityFromUrl($file); + break; + default: + $entity = $service->createEntityFromDocxAst($ast); + } + + $flatten = $service->flattenAst($ast->content); + $content->create($flatten, $entity); + $contentImportLogger->createEntry($ast, $entity, $file->getFileUri()); + $form_state->setRedirectUrl($entity->toUrl('edit-form')); + } + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/DefaultImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/DefaultImportPlugin.php new file mode 100644 index 000000000..dc96ba9fe --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/DefaultImportPlugin.php @@ -0,0 +1,120 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_default", + * label = @Translation("Default convert plugin"), + * weight = 0, + * ) + */ +class DefaultImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Default converter, just throws the html into Gutenberg blocks.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + return [ + 'htmlValue' => 'string, html markup', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + return <<<EOD + <!-- wp:paragraph --> + htmlValue + <!-- /wp:paragraph --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + // This should not much by default. + return FALSE; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + // We are using some custom method here. + $data['htmlValue'] = $chunk['htmlValue']; + return $this->generateBlock($data); + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/HeaderImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/HeaderImportPlugin.php new file mode 100644 index 000000000..2e8910fc6 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/HeaderImportPlugin.php @@ -0,0 +1,184 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a markdown header to gutenberg block convert plugin. + * + * @Plugin( + * id = "ai_header", + * label = @Translation("Markdown header convert plugin"), + * weight = 0, + * ) + */ +class HeaderImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Convert markdown headers to Gutenberg blocks.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + // Actually we are only interest here in keys. + // Values can be descriptions to help the data extraction if using AI. + return [ + 'attributesJson' => 'json attributes string', + 'headingText' => 'string', + 'headingLevel' => 'number, 2 or 3 or 4.', + 'headerHtmlTag' => 'h2 or h3 or h4. Any other header should be converted to h2.', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + return <<<EOD + <!-- wp:custom/heading attributesJson --> + <headerHtmlTag class="wp-block-custom-heading">headingText</headerHtmlTag> + <!-- /wp:custom/heading --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + return $chunk['type'] == 'Header'; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + // We are using some custom method here. + // @todo Add a validation method. + $data = $this->parseMarkdownHeader($chunk['raw']); + return $this->generateBlock($data); + } + + /** + * Parses a markdown header string and returns details about it. + * + * This method trims whitespace from the header string, matches a specific + * markdown heading pattern, determines the heading level based on the number + * of '#' characters, and generates the corresponding HTML tag for the header. + * + * The resulting heading level is constrained between 2 and 4 based on custom rules. + * + * @param string $header + * The markdown header string to parse. + * + * @return array + * An associative array. + * + * @throws \InvalidArgumentException if the header is not in a valid markdown format. + */ + private function parseMarkdownHeader(string $header): array { + // Trim whitespace. + $header = trim($header); + + // Match the heading pattern. + if (!preg_match('/^(#{1,6})\s+(.+)$/', $header, $matches)) { + throw new \InvalidArgumentException('Invalid markdown header format'); + } + + // Get the number of # symbols to determine heading level. + $level = strlen($matches[1]); + + // Restrictions from the custom/header SLB block. + if ($level == 1) { + $level = 2; + } + + if ($level > 4) { + $level = 4; + } + + // Get the actual heading text. + $text = trim($matches[2]); + // $text = str_replace('"', '', $text); + // $text = str_replace('\\', '', $text); + // Create the corresponding HTML tag. + $headerHtmlTag = 'h' . $level; + + $attributesJson = [ + 'level' => intval($level), + 'text' => $text, + ]; + + return [ + 'attributesJson' => json_encode($attributesJson), + 'headingText' => $text, + 'headingLevel' => $level, + 'headerHtmlTag' => $headerHtmlTag, + ]; + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ImageImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ImageImportPlugin.php new file mode 100644 index 000000000..945c82aac --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ImageImportPlugin.php @@ -0,0 +1,204 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a markdown image to gutenberg block convert plugin. + * + * @Plugin( + * id = "ai_image", + * label = @Translation("Markdown image convert plugin"), + * weight = 0, + * ) + */ +class ImageImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + private const PUBLISHED = 1; + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Convert markdown images to Gutenberg blocks.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + return [ + 'mediaId' => 'number', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + return <<<EOD + <!-- wp:custom/image-with-text {"mediaEntityIds":["mediaId"]} --> + <!-- wp:paragraph --> + <p></p> + <!-- /wp:paragraph --> + <!-- /wp:custom/image-with-text --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + return $chunk['type'] == 'Image'; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + // We are using some custom method here. + // @todo Add a validation method. + $src = $chunk['src']; + $media = $this->createMediaImageFromPath($src); + $data = ['mediaId' => '']; + if ($media) { + $data = ['mediaId' => $media->id()]; + } + return $this->generateBlock($data); + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } + + /** + * Creates a file entity and a media image entity from a given image path. + * + * @param string $image_path + * The server path to the image file. + * @param string $media_bundle + * The media bundle type (optional, defaults to 'image'). + * @param int $user_id + * The user ID to associate with the created entities (optional, defaults to current user). + * + * @return \Drupal\media\MediaInterface|null + * The created media entity, or NULL if creation fails. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function createMediaImageFromPath($image_path, $media_bundle = 'image', $user_id = NULL) { + // Ensure the file exists. + if (!file_exists($image_path)) { + \Drupal::logger('image_import')->error('File does not exist: @path', ['@path' => $image_path]); + return NULL; + } + + // Get the current user if no user ID is provided. + if ($user_id === NULL) { + $user_id = \Drupal::currentUser()->id(); + } + + // Prepare the file. + $file_uri = 'public://' . basename($image_path); + + try { + // Create file entity. + $file = \Drupal::service('file.repository')->writeData( + file_get_contents($image_path), + $file_uri, + FileSystemInterface::EXISTS_REPLACE + ); + + // Set file status to permanent. + if ($file) { + $file->setPermanent(); + $file->save(); + } + else { + \Drupal::logger('image_import')->error('Failed to create file entity for: @path', ['@path' => $image_path]); + return NULL; + } + + // Create media entity. + $media_storage = \Drupal::entityTypeManager()->getStorage('media'); + /** @var \Drupal\media\Entity\media $media */ + $media = $media_storage->create([ + 'bundle' => $media_bundle, + 'name' => $file->getFilename(), + 'uid' => $user_id, + 'status' => self::PUBLISHED, + 'field_media_image' => [ + 'target_id' => $file->id(), + // @todo Improve alt text generation. + 'alt' => $file->getFilename(), + 'title' => $file->getFilename(), + ], + ]); + + $media->save(); + return $media; + } + catch (\Exception $e) { + \Drupal::logger('image_import')->error('Error creating media entity: @error', ['@error' => $e->getMessage()]); + return NULL; + } + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ListImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ListImportPlugin.php new file mode 100644 index 000000000..44ef7227f --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ListImportPlugin.php @@ -0,0 +1,136 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_list", + * label = @Translation("Default list convert plugin"), + * weight = 0, + * ) + */ +class ListImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('List converter, markdown into Gutenberg block.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + return [ + 'listItems' => 'string, html markup', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + $config = $this->configuration; + $chunk = $config['chunk']; + + if (isset($chunk['ordered']) + && $chunk['ordered'] == TRUE) { + return <<<EOD + <!-- wp:list {"ordered":true} --> + listItems + <!-- /wp:list --> + EOD; + } + + return <<<EOD + <!-- wp:list --> + listItems + <!-- /wp:list --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + return $chunk['type'] == 'List'; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + + $html = $chunk['htmlValue']; + $html = str_replace(["\r", "\n"], '', $html); + + $data['listItems'] = $html; + + return $this->generateBlock($data); + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ParagraphImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ParagraphImportPlugin.php new file mode 100644 index 000000000..61ad647c5 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/ParagraphImportPlugin.php @@ -0,0 +1,120 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_paragraph", + * label = @Translation("Default paragraph convert plugin"), + * weight = 0, + * ) + */ +class ParagraphImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Paragraph converter for Gutenberg blocks.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + return [ + 'paragraphText' => 'string, html markup', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + return <<<EOD + <!-- wp:paragraph --> + paragraphText + <!-- /wp:paragraph --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + return $chunk['type'] == 'Paragraph'; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + $val = $chunk['htmlValue']; + $data['paragraphText'] = preg_replace('/<img[^>]+>/i', '', $val); + return $this->generateBlock($data); + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/TableImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/TableImportPlugin.php new file mode 100644 index 000000000..5dcc9ffa7 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiImport/TableImportPlugin.php @@ -0,0 +1,119 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_table", + * label = @Translation("Default table convert plugin"), + * weight = 0, + * ) + */ +class TableImportPlugin extends PluginBase implements AiImportPluginManagerInterface { + + /** + * The schema to use. + * + * @var array + */ + private array $schema = []; + + /** + * The template to use. + * + * @var string + */ + private string $template; + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + $this->schema = $this->getSchema(); + $this->template = $this->getTemplate(); + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Table converter, markdown into Gutenberg block.'); + } + + /** + * {@inheritDoc} + */ + public function getSchema() { + return [ + 'htmlTable' => 'string, html markup', + ]; + } + + /** + * {@inheritDoc} + */ + public function getTemplate() { + return <<<EOD + <!-- wp:table --> + <figure class="wp-block-table">htmlTable</figure> + <!-- /wp:table --> + EOD; + } + + /** + * {@inheritDoc} + */ + public function matches(array $chunk) { + return $chunk['type'] == 'Table'; + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunk) { + $data['htmlTable'] = $chunk['htmlValue']; + return $this->generateBlock($data); + } + + /** + * {@inheritDoc} + */ + public function generateBlock(array $data): string { + // Validate required keys. + $required_keys = array_keys($this->getSchema()); + $template = $this->getTemplate(); + foreach ($required_keys as $key) { + if (!isset($data[$key])) { + throw new \InvalidArgumentException("Missing required key: {$key}"); + } + } + + // Create replacement pairs. + foreach ($required_keys as $key) { + $replacements[$key] = $data[$key]; + } + + // Perform replacements. + foreach ($replacements as $key => $value) { + $template = str_replace($key, $value, $template); + } + + return $template; + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/EmptyChunksRemovePostImportPlugin.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/EmptyChunksRemovePostImportPlugin.php new file mode 100644 index 000000000..a951d1935 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/EmptyChunksRemovePostImportPlugin.php @@ -0,0 +1,55 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiPostImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiPostImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_empty_chunks", + * label = @Translation("Fllters empty chunks"), + * weight = 0, + * ) + */ +class EmptyChunksRemovePostImportPlugin extends PluginBase implements AiPostImportPluginManagerInterface { + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Filters empty chunks.'); + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunks) { + $chunks = array_filter($chunks, function ($item) { + if (str_starts_with(trim($item), '<!-- wp:paragraph -->')) { + $cleaned = $cleaned = preg_replace('/[\r\n]+/', '', strip_tags($item)); + return strlen($cleaned) > 0; + } + return TRUE; + }); + return $chunks; + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/ImageCaptionPostImport.php b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/ImageCaptionPostImport.php new file mode 100644 index 000000000..2f4527e45 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_import/src/Plugin/AiPostImport/ImageCaptionPostImport.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\silverback_ai_import\Plugin\AiPostImport; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\silverback_ai_import\AiPostImportPluginManagerInterface; + +/** + * Provides a default gutenberg block converter plugin. + * + * @Plugin( + * id = "ai_image_captions", + * label = @Translation("Handleds media image captions."), + * weight = 0, + * ) + */ +class ImageCaptionPostImport extends PluginBase implements AiPostImportPluginManagerInterface { + + /** + * Constructs a \Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + } + + /** + * Get a description if the plugin. + */ + public function description() { + return $this->t('Handles media image captions'); + } + + /** + * {@inheritDoc} + */ + public function convert(array $chunks) { + // @todo Find a better way to explode/implode chunks + // @todo Add DI + // $ai = \Drupal::service('silverback_ai_import.content'); + // $input = implode('\n', $chunks); + // $out = $ai->request($this->getPrompt($input)); + return $chunks; + } + + private function getPrompt(string $input) { + // @todo + $prompt = <<<EOD + Process Gutenberg editor blocks to properly place image captions within image-with-text components. + + Input: + $input + + ## Input Requirements + - Gutenberg blocks + - May contain wp:custom/image-with-text blocks + - May contain standalone paragraph blocks with image captions + + ## Processing Rules + 1. Scan for potential image captions: + - Located in paragraph blocks + - Follows immediately after wp:custom/image-with-text blocks + - Usually in italic format (`<em>` tags) + + 2. Caption Integration: + - Move identified captions inside wp:custom/image-with-text blocks + - Replace only existing empty paragraph tags (`<p></p>`) + - Maintain original caption formatting + + 3. Structure Preservation: + - Keep all other content unchanged + - Maintain valid Gutenberg block syntax + - Preserve all block attributes and properties + + ## Output Format + Return complete Gutenberg block structure with integrated captions and nothing else. + + ## Validation + - Ensure output maintains valid Gutenberg syntax + - Verify all captions are properly nested + - Confirm no content loss during transformation + EOD; + + return $prompt; + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_ai_test/tests/src/Unit/ExampleTest.php b/packages/drupal/silverback_ai/modules/silverback_ai_test/tests/src/Unit/ExampleTest.php new file mode 100644 index 000000000..5a01a5081 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_ai_test/tests/src/Unit/ExampleTest.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\silverback_ai_test\Unit; + +use Drupal\Tests\UnitTestCase; + +/** + * Test description. + * + * @group silverback_ai_test + */ +final class ExampleTest extends UnitTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // @todo Mock required classes here. + } + + /** + * Tests something. + */ + public function testSomething(): void { + self::assertTrue(TRUE, 'This is TRUE!'); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/README.md b/packages/drupal/silverback_ai/modules/silverback_image_ai/README.md new file mode 100644 index 000000000..7ad2af508 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/README.md @@ -0,0 +1,34 @@ +## INTRODUCTION + +The Silverback Image AI module provides AI-powered functionality for image management in Drupal. Its main features include: + +- Automatic generation of alt text for images using AI +- Intelligent image analysis and description +- Accessibility improvements through better image descriptions +- Integration with OpenAI's vision models for image processing + +The module aims to enhance the accessibility and SEO of your Drupal site by ensuring all images have meaningful alternative text. + +## REQUIREMENTS + +- Silveback AI module + +## INSTALLATION + +Install as you would normally install a contributed Drupal module. +See: <https://www.drupal.org/node/895232> for further information. + +## CONFIGURATION + +- Base settings form: `/admin/config/system/silverback/image-ai-settings`. + +## SERVICES + +### ImageAiUtilities Service + +The `ImageAiUtilities` service provides core functionality for AI-powered image processing. It handles: + +- Generation of ALT text for images using OpenAI's vision models +- Processing of image files and media entities +- Integration with OpenAI's API for image analysis +- Token usage tracking and logging diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/config/install/silverback_image_ai.settings.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/install/silverback_image_ai.settings.yml new file mode 100644 index 000000000..011175f09 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/install/silverback_image_ai.settings.yml @@ -0,0 +1,7 @@ +open_ai_base_uri: 'https://api.openai.com/v1/' +open_ai_key: '' +ai_model: '' +words_length: 30 +alt_ai_context: 'Silverback is a PHP and Javascript framework to generate decoupled web sites.' +debug_mode: 0 +alt_disclaimer: 'The alternative text is generated by artificial intelligence. Verify for accuracy before publishing.' diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/config/optional/system.action.media_alt_ai_update_action.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/optional/system.action.media_alt_ai_update_action.yml new file mode 100644 index 000000000..0fcad06e4 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/optional/system.action.media_alt_ai_update_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_alt_ai_update_action +label: 'Alt text update (imaged only)' +type: media +plugin: entity:alt_ai_update_action:media +configuration: { } diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/config/schema/silverback_image_ai.schema.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/schema/silverback_image_ai.schema.yml new file mode 100644 index 000000000..ddd1e7092 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/config/schema/silverback_image_ai.schema.yml @@ -0,0 +1,19 @@ +silverback_image_ai.settings: + type: config_object + label: 'Silverback Image AI settings' + mapping: + ai_model: + label: 'Model' + type: string + words_length: + label: 'Number of ALT text words to generate' + type: integer + alt_ai_context: + label: 'Context' + type: text + debug_mode: + label: 'Debug mode' + type: boolean + alt_disclaimer: + label: 'Disclaimer text' + type: text diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/drush.services.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/drush.services.yml new file mode 100644 index 000000000..bbfff79d8 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/drush.services.yml @@ -0,0 +1,6 @@ +services: + silverback_image_ai.commands: + class: \Drupal\silverback_image_ai\Drush\Commands\SilverbackImageAiCommands + arguments: ['@entity_type.manager', '@silverback_image_ai.batch.updater', '@silverback_image_ai.utilities'] + tags: + - { name: drush.command } diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.config_translation.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.config_translation.yml new file mode 100644 index 000000000..66521d111 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.config_translation.yml @@ -0,0 +1,5 @@ +silverback_image_ai.config: + title: 'Silverback Image AI settings' + base_route_name: silverback_image_ai.settings + names: + - silverback_image_ai.settings diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.info.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.info.yml new file mode 100644 index 000000000..447786720 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.info.yml @@ -0,0 +1,7 @@ +name: 'Silverback Alt AI' +type: module +description: 'Silverback AI utilities for images' +package: Silverback +core_version_requirement: ^10 || ^11 +dependencies: + - silverback_ai:silverback_ai diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.install b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.install new file mode 100644 index 000000000..5f303f2f0 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.install @@ -0,0 +1,6 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Silveback Alt AI module. + */ diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.action.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.action.yml new file mode 100644 index 000000000..daf02196c --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.action.yml @@ -0,0 +1,7 @@ +media.alt_update: + route_name: silverback_image_ai.image_ai_batch_update + title: 'Alt text update (AI)' + weight: 12 + appears_on: + - entity.media.collection + - view.media_library.page diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.menu.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.menu.yml new file mode 100644 index 000000000..b7c811bc1 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.menu.yml @@ -0,0 +1,6 @@ +silverback_image_ai.alt_ai_settings: + title: Silverback Image AI settings + description: Settings for the Silverback image AI utilities. + parent: silverback_ai.admin_config_ai + route_name: silverback_image_ai.settings + weight: 10 diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.task.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.task.yml new file mode 100644 index 000000000..969192aa2 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.links.task.yml @@ -0,0 +1,5 @@ +silverback_image_ai.settings: + title: 'Settings' + weight: 0 + route_name: silverback_image_ai.settings + base_route: silverback_image_ai.settings diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.module b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.module new file mode 100644 index 000000000..cee07cce3 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.module @@ -0,0 +1,6 @@ +<?php + +/** + * @file + * Primary module hooks for Silveback Alt AI module. + */ diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.routing.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.routing.yml new file mode 100644 index 000000000..b3ed7f4cd --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.routing.yml @@ -0,0 +1,15 @@ +silverback_image_ai.settings: + path: '/admin/config/system/silverback/image-ai-settings' + defaults: + _title: 'Silverback Image AI Settings' + _form: 'Drupal\silverback_image_ai\Form\ImageAiSettingsForm' + requirements: + _permission: 'administer site configuration' + +silverback_image_ai.image_ai_batch_update: + path: '/admin/silverback-ai/update/image' + defaults: + _title: 'Image AI Batch Update' + _form: 'Drupal\silverback_image_ai\Form\ImageAiBatchUpdateForm' + requirements: + _permission: 'access content' diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.services.yml b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.services.yml new file mode 100644 index 000000000..c02ab45d2 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/silverback_image_ai.services.yml @@ -0,0 +1,8 @@ +services: + silverback_image_ai.utilities: + class: Drupal\silverback_image_ai\ImageAiUtilities + arguments: ['@logger.factory','@config.factory', '@http_client', '@silverback_ai.token.usage', '@silverback_ai.openai_http_client', '@entity_type.manager'] + silverback_image_ai.batch.updater: + class: 'Drupal\silverback_image_ai\MediaUpdaterBatch' + arguments: + - '@logger.factory' diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Drush/Commands/SilverbackImageAiCommands.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Drush/Commands/SilverbackImageAiCommands.php new file mode 100644 index 000000000..0f4b0e2a6 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Drush/Commands/SilverbackImageAiCommands.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\silverback_image_ai\Drush\Commands; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\silverback_image_ai\ImageAiUtilities; +use Drupal\silverback_image_ai\MediaUpdaterBatch; +use Drush\Attributes as CLI; +use Drush\Commands\DrushCommands; +use Drush\Exceptions\UserAbortException; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A Drush commandfile. + */ +final class SilverbackImageAiCommands extends DrushCommands { + + /** + * Constructs a SilverbackImageAiCommands object. + */ + public function __construct( + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly MediaUpdaterBatch $batch, + private readonly ImageAiUtilities $service + ) { + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('silverback_image_ai.batch.updater'), + $container->get('silverback_image_ai.utilities'), + ); + } + + /** + * Command description here. + * + * @param false[] $options + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drush\Exceptions\UserAbortException + */ + #[CLI\Command(name: 'silverback-image-ai:alt:generate', aliases: ['slb:alt:g'])] + #[CLI\Option(name: 'update-all', description: 'Update all image alt texts. ATTENTION: This will overwrite existing alt texts.')] + #[CLI\Usage(name: 'silverback-image-ai:alt:generate', description: 'Generate alt text for media images.')] + public function commandName(array $options = [ + 'update-all' => FALSE, + ]) { + $media_entities = []; + if ($options['update-all']) { + $this->io()->warning(dt('ATTENTION: This action will overwrite all existing media image alt texts.')); + if ($this->io()->confirm(dt('Are you sure you want to update all existing alt texts?'), FALSE)) { + $media_entities = $this->service->getMediaEntitiesToUpdateAll(); + $this->batch->create($media_entities); + } + else { + throw new UserAbortException(); + } + } + else { + try { + $media_entities = $this->service->getMediaEntitiesToUpdateWithAlt(); + $this->batch->create($media_entities); + } + catch (InvalidPluginDefinitionException | PluginNotFoundException $e) { + // @todo + } + } + + $this->logger()->success(dt('@count media images updated.', [ + '@count' => count($media_entities), + ])); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiBatchUpdateForm.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiBatchUpdateForm.php new file mode 100644 index 000000000..a9d9575b1 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiBatchUpdateForm.php @@ -0,0 +1,162 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_image_ai\Form; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\silverback_image_ai\ImageAiUtilities; +use Drupal\silverback_image_ai\MediaUpdaterBatch; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a Silverback Alt AI form. + */ +final class ImageAiBatchUpdateForm extends FormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'silverback_image_ai_image_ai_batch_update'; + } + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The batch service. + * + * @var \Drupal\silverback_image_ai\MediaUpdaterBatch + */ + protected $batch; + + /** + * The batch service. + * + * @var \Drupal\silverback_image_ai\ImageAiUtilities + */ + protected $service; + + /** + * Constructor. + * + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + * @param \Drupal\silverback_image_ai\MediaUpdaterBatch $batch + * THe batch service. + * @param \Drupal\silverback_image_ai\ImageAiUtilities $service + * THe batch service. + */ + public function __construct(MessengerInterface $messenger, MediaUpdaterBatch $batch, ImageAiUtilities $service) { + $this->messenger = $messenger; + $this->batch = $batch; + $this->service = $service; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('messenger'), + $container->get('silverback_image_ai.batch.updater'), + $container->get('silverback_image_ai.utilities'), + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + $form['description'] = [ + '#markup' => '<p>This form will run batch processing.</p>', + ]; + + $form['batch_container'] = [ + '#type' => 'details', + '#title' => $this->t('Media imaged ALT text batch update'), + '#open' => TRUE, + ]; + + // $form['batch_missing_only']['actions']['#type'] = 'actions'; + $missing_alt_count = $this->service->getMediaEntitiesToUpdateWithAlt(); + $media_images_count = $this->service->getMediaEntitiesToUpdateAll(); + + $form['batch_container']['info'] = [ + '#type' => 'markup', + '#markup' => $this->t('There are <strong><em>@count/@total</em></strong> media images with missing alt text.', [ + '@count' => count($missing_alt_count), + '@total' => count($media_images_count), + ]), + ]; + + $form['batch_container']['selection'] = [ + '#type' => 'radios', + '#title' => $this->t('Select what to update'), + '#default_value' => 1, + '#options' => [ + 1 => $this->t('Update only media images with missing ALT text'), + 2 => $this->t('Update all media images'), + ], + ]; + + $form['batch_container']['confirm'] = [ + '#title' => $this->t('⚠️ I understand that this action will overwrite all existing ALT texts and I want to proceed.'), + '#type' => 'checkbox', + '#states' => [ + 'visible' => [ + [ + ':input[name="selection"]' => ['value' => 2], + ], + ], + ], + ]; + + $form['batch_container']['actions']['submit_all'] = [ + '#type' => 'submit', + '#value' => $this->t('Run update process'), + '#button_type' => 'primary', + '#states' => [ + 'disabled' => [ + [ + ':input[name="confirm"]' => ['checked' => FALSE], + ], + ], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + // .. + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + // @todo Create a method for this + try { + $media_entities = $this->service->getMediaEntitiesToUpdateAll(); + $this->batch->create($media_entities); + } + catch (InvalidPluginDefinitionException | PluginNotFoundException $e) { + // @todo + } + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiSettingsForm.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiSettingsForm.php new file mode 100644 index 000000000..ee51793c0 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Form/ImageAiSettingsForm.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_image_ai\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Configure Silverback Alt AI settings for this site. + */ +final class ImageAiSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'silverback_image_ai_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['silverback_image_ai.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + $open_ai_api_key = $this->configFactory->get('silverback_ai.settings')->get('open_ai_api_key'); + + // .. + if (!$open_ai_api_key) { + $url = Url::fromRoute('silverback_ai.ai_settings'); + \Drupal::messenger()->addWarning($this->t('Open AI API key is missing. <strong><a href="@link">Click here</a></strong> to add your key.', [ + '@link' => $url->toString(), + ])); + } + + $form['credentials'] = [ + '#type' => 'details', + '#title' => $this->t('Open AI model'), + '#open' => TRUE, + ]; + + // @todo Make this dynamically + $form['credentials']['ai_model'] = [ + '#type' => 'select', + '#title' => $this->t('Model'), + '#options' => [ + 'gpt-4o-mini' => 'gpt-4o-mini', + 'gpt-4o-mini-2024-07-18' => 'gpt-4o-mini-2024-07-18', + ], + '#empty_option' => $this->t('- Select model -'), + '#description' => $this->t('Leave empty to use the default <strong><em>gpt-4o-mini</em></strong> model.') . '<br />' . + $this->t('<strong><a href="@href" target="_blank">Learn more</a></strong> about the models.', [ + '@href' => 'https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence', + ]), + ]; + + $form['general'] = [ + '#type' => 'details', + '#title' => $this->t('General settings'), + '#open' => TRUE, + ]; + + $form['general']['debug_mode'] = [ + '#title' => $this->t('Debug mode'), + '#type' => 'checkbox', + '#default_value' => $this->configFactory->get('silverback_image_ai.settings')->get('debug_mode') ?? FALSE, + ]; + + $form['general']['words_length'] = [ + '#type' => 'number', + '#title' => $this->t('Number of ALT text words to generate'), + '#description' => $this->t('Define the number of ALT text words to be generated. Should be between 40 and 60 words.'), + '#min' => 10, + '#max' => 40, + '#default_value' => $this->config('silverback_image_ai.settings')->get('words_length') ?? 30, + '#field_suffix' => $this->t(' words'), + ]; + + $form['general']['alt_disclaimer'] = [ + '#type' => 'textfield', + '#title' => $this->t('Disclaimer text'), + '#default_value' => $this->config('silverback_image_ai.settings')->get('alt_disclaimer') + ?? $this->t('The ALT contents are generated by artificial intelligence. Verify for accuracy before publishing.'), + '#description' => $this->t("Define a disclaimer text for the editors. Keep it short."), + ]; + + $form['general']['alt_ai_context'] = [ + '#type' => 'textarea', + '#title' => $this->t('Context'), + '#rows' => 3, + '#access' => TRUE, + '#default_value' => $this->config('silverback_image_ai.settings')->get('alt_ai_context'), + '#description' => $this->t('Optionally, you can use a context to generate your ALT text. Keep it short and precise.'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('silverback_image_ai.settings') + ->set('ai_model', $form_state->getValue('ai_model')) + ->set('debug_mode', $form_state->getValue('debug_mode')) + ->set('words_length', intval($form_state->getValue('words_length'))) + ->set('alt_disclaimer', trim($form_state->getValue('alt_disclaimer'))) + ->set('alt_ai_context', trim($form_state->getValue('alt_ai_context'))) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilities.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilities.php new file mode 100644 index 000000000..529d25688 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilities.php @@ -0,0 +1,398 @@ +<?php + +namespace Drupal\silverback_image_ai; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\file\FileInterface; +use Drupal\media\MediaInterface; +use Drupal\silverback_ai\HttpClient\OpenAiHttpClient; +use Drupal\silverback_ai\TokenUsageInterface; +use Psr\Http\Client\ClientInterface; + +/** + * @todo Add class description. + */ +final class ImageAiUtilities implements ImageAiUtilitiesInterface { + + private const DEFAULT_AI_MODEL = 'gpt-4o-mini'; + private const DEFAULT_WORD_LENGTH = 40; + + use StringTranslationTrait; + + /** + * Constructs an AltAiGenerator object. + */ + public function __construct( + private readonly LoggerChannelFactoryInterface $loggerFactory, + private readonly ConfigFactoryInterface $configFactory, + private readonly ClientInterface $httpClient, + private readonly TokenUsageInterface $silverbackAiTokenUsage, + private readonly OpenAiHttpClient $openAiHttpClient, + private readonly EntityTypeManager $entityTypeManager, + ) { + } + + /** + * Generates an ALT text for an image using the OpenAI API. + * + * This method takes an image file and a language code to generate + * a descriptive ALT text for the image. It utilizes the OpenAI API + * for ALT text generation if the API key is available. + * + * @param \Drupal\file\FileInterface $image + * The image file for which ALT text needs to be generated. + * @param string $langcode + * The language code representing the language in which the ALT text + * should be generated. Defaults to English if not specified or invalid. + * + * @return string|null + * The generated ALT text if successful, a message indicating a missing API key, + * or NULL if the API response does not contain the expected data. + * + * @throws \Exception + * + * @todo + * Implement a fallback mechanism to return default ALT text in case of API failure. + */ + public function generateImageAlt(FileInterface $image, string $langcode) { + + $base_64_data = $this->getBase64EncodeData($image); + + if (getenv('SILVERBACK_IMAGE_AI_DRY_RUN')) { + $response_body = $this->getFakeResponseBody($base_64_data, $langcode); + } else { + $response_body = $this->sendOpenAiRequest($base_64_data, $langcode); + } + + $this->logUsage($response_body, $image); + + if ($this->configFactory->get('silverback_image_ai.settings')->get('debug_mode')) { + \Drupal::logger('debug')->debug('<pre>' . print_r($response_body, TRUE) . "</pre>"); + } + + if (isset($response_body['choices'][0]['message']['content'])) { + return trim($response_body['choices'][0]['message']['content']); + } + + return NULL; + } + + /** + * Converts an image file to a base64-encoded string. + * + * This method takes an image file represented by a FileInterface object, + * processes it through a specified image style to ensure the desired derivative + * is created, and then returns the image data encoded in base64 format, + * suitable for embedding in HTML. + * + * @param \Drupal\file\FileInterface $image + * The image file object for which the base64 data needs to be generated. + * + * @return string + * A string containing the base64-encoded image data prefixed with the + * appropriate data URI scheme and mime type. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * + * @todo + * Extract the image processing logic to a separate method for improved + * code maintainability and readability. + */ + public function getBase64EncodeData(FileInterface $image) { + // @todo Extract this to method + $image_uri = $image->getFileUri(); + $image_type = $image->getMimeType(); + $fileSystem = \Drupal::service('file_system'); + + /** @var \Drupal\image\ImageStyleInterface $image_style */ + $image_style = \Drupal::entityTypeManager()->getStorage('image_style')->load('large'); + + // Create image derivatives if they not already exists. + if ($image_style) { + $derivative_uri = $image_style->buildUri($image_uri); + if (!file_exists($derivative_uri)) { + $image_style->createDerivative($image_uri, $derivative_uri); + } + $absolute_path = $fileSystem->realpath($derivative_uri); + } else { + $absolute_path = $fileSystem->realpath($image_uri); + } + + $image_file = file_get_contents($absolute_path); + $base_64_image = base64_encode($image_file); + return "data:$image_type;base64,$base_64_image"; + } + + /** + * Sends a request to the OpenAI API to generate ALT text for an image. + * + * This method takes base64-encoded image data and a language code as parameters. + * It constructs a payload for the OpenAI API using the specified model and message format, + * including an instruction to generate a concise ALT text for the image in the specified language. + * + * @param string $base_64_data + * The base64-encoded data of the image for which to generate ALT text. + * @param string $langcode + * The language code for the language in which the ALT text should be generated. + * + * @return array + * The decoded JSON response from the OpenAI API containing the generated ALT text. + * + * @throws \Exception|\GuzzleHttp\Exception\GuzzleException + * Thrown if the HTTP request to the OpenAI API fails. + */ + public function sendOpenAiRequest(string $base_64_data, string $langcode) { + $language_name = $langcode ? \Drupal::languageManager()->getLanguageName($langcode) : 'English'; + // @todo Get some of these from settings + $model = $this->configFactory->get('silverback_image_ai.settings')->get('ai_model') ?: self::DEFAULT_AI_MODEL; + $words = $this->configFactory->get('silverback_image_ai.settings')->get('words_length') ?: self::DEFAULT_WORD_LENGTH; + + $context = $this->configFactory->get('silverback_image_ai.settings')->get('alt_ai_context'); + + if (!empty($context)) { + $prompt = "Given the following context:\r\n'{$context}' \r\n"; + $prompt .= "generate a concise and descriptive ALT text for this image. The ALT text should be a single sentence, no more than {$words} words long. The Alt text should be in the {$language_name} language."; + } else { + $prompt = "Generate a concise and descriptive ALT text for this image. The ALT text should be a single sentence, no more than {$words} words long. The Alt text should be in the {$language_name} language."; + } + + $payload = [ + 'model' => $model, + 'messages' => [ + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => $prompt, + ], + [ + 'type' => 'image_url', + 'image_url' => [ + "url" => $base_64_data, + ], + ], + ], + ], + ], + 'max_tokens' => 100, + ]; + + try { + $response = $this->openAiHttpClient->post('chat/completions', [ + 'json' => $payload, + ]); + } catch (\Exception $e) { + throw new \Exception('HTTP request failed: ' . $e->getMessage()); + } + + $responseBodyContents = $response->getBody()->getContents(); + return json_decode($responseBodyContents, TRUE, 512, JSON_THROW_ON_ERROR); + } + + /** + * Number of media entities with the 'image' bundle that are missing alt text. + * + * @return int + * The number of media entities missing alt text. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * + * @todo Create a db table to store data, query can be slow for large number of entities. + */ + public function getMissingAltEntitiesCount() { + $count = 0; + // @todo Add DI + $media_entities = \Drupal::entityTypeManager()->getStorage('media')->loadByProperties([ + 'bundle' => 'image', + ]); + foreach ($media_entities as $media) { + /** @var \Drupal\media\Entity\Media $media */ + foreach ($media->getTranslationLanguages() as $langcode => $translation) { + $entity = $media->getTranslation($langcode); + if (!$entity->field_media_image->alt) { + $count++; + } + } + } + return $count; + } + + /** + * Sets the alt text for the media image field. + * + * This method updates the alt text of the given media entity's image field. + * It saves the changes to the entity unless the 'SILVERBACK_IMAGE_AI_DRY_RUN' environment + * variable is set. The method is intended for use with Drupal media entities. + * + * @param \Drupal\media\Entity\Media $media + * The media entity whose image alt text is being set. + * @param string $alt_text + * The alt text to set for the media image. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function setMediaImageAltText(MediaInterface $media, string $alt_text) { + /** @var \Drupal\media\Entity\Media $media */ + $media->field_media_image->alt = $alt_text; + if (!getenv('SILVERBACK_IMAGE_AI_DRY_RUN')) { + $media->save(); + } + } + + /** + * Emulates a fake response. Used for development. + */ + public function getFakeResponseBody(string $base_64_data, string $langcode) { + return [ + "id" => "chatcmpl-AJe6memR1kLukQdK957wAFydW54rK", + "object" => "chat.completion", + "created" => 1729245772, + "model" => "gpt-4o-mini", + "choices" => [ + 0 => [ + "index" => 0, + "message" => [ + "role" => "assistant", + "content" => "A group of three people collaborating around a table with laptops and data displays.", + "refusal" => NULL, + ], + "logprobs" => NULL, + "finish_reason" => "stop", + ], + ], + "usage" => [ + "prompt_tokens" => 25536, + "completion_tokens" => 15, + "total_tokens" => 25551, + "prompt_tokens_details" => [ + "cached_tokens" => 0, + ], + "completion_tokens_details" => [ + "reasoning_tokens" => 0, + ], + ], + "system_fingerprint" => "fp_8552ec53e1", + ]; + } + + /** + * Retrieves the total count of media items of type 'image'. + * + * This function executes a database query to count the distinct media items + * where the bundle is 'image' and the media ID (mid) is not null. + * + * @return int + * The total count of image media items. + */ + public function getMediaImagesTotalCount() { + $query = \Drupal::database()->select('media', 'm') + ->fields('m', ['mid']) + ->condition('bundle', 'image') + ->isNotNull('mid') + ->distinct(); + return (int) $query->countQuery()->execute()->fetchField(); + } + + /** + * Gets a list of media entities. + * + * This function loads media entities of the 'image' bundle and iterates over + * their translations. It builds and returns an array of entities with language + * codes. + * + * @return array + * An array of arrays, each containing: + * - entity: The media entity translation. + * - langcode: The language code of the translation. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getMediaEntitiesToUpdateAll() { + $entities = []; + $media_entities = $this->entityTypeManager->getStorage('media')->loadByProperties([ + 'bundle' => 'image', + ]); + foreach ($media_entities as $media) { + /** @var \Drupal\media\Entity\Media $media */ + foreach ($media->getTranslationLanguages() as $langcode => $translation) { + $entity = $media->getTranslation($langcode); + $entities[] = [ + 'entity' => $entity, + 'langcode' => $langcode, + ]; + } + } + return $entities; + } + + /** + * Gets a list of media entities to update without alt value. + * + * This function loads media entities of the 'image' bundle and iterates over + * their translations. It builds and returns an array of entities with language + * codes. + * + * @return array + * An array of arrays, each containing: + * - entity: The media entity translation. + * - langcode: The language code of the translation. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getMediaEntitiesToUpdateWithAlt() { + $entities = []; + $media_entities = $this->entityTypeManager->getStorage('media')->loadByProperties([ + 'bundle' => 'image', + ]); + foreach ($media_entities as $media) { + /** @var \Drupal\media\Entity\Media $media */ + foreach ($media->getTranslationLanguages() as $langcode => $translation) { + $entity = $media->getTranslation($langcode); + if (!$entity->field_media_image->alt) { + $entities[] = [ + 'entity' => $entity, + 'langcode' => $langcode, + ]; + } + } + } + return $entities; + } + + /** + * Logs the usage of the Silverback Image AI module. + * + * This method updates the response body with module and entity details and + * creates a new usage entry using the Silverback AI Token Usage service. + * + * @param array $response_body + * An associative array that will be enhanced with module and entity information. + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * The entity for which to log usage details. If provided, its id, type, + * and revision id will be added to the response body if the entity is revisionable. + * + * @throws \Exception + */ + public function logUsage(array $response_body, EntityInterface $entity = NULL) { + $response_body['module'] = 'Silverback Image AI'; + + if ($entity) { + $response_body['entity_id'] = (string) $entity->id(); + $response_body['entity_type_id'] = (string) $entity->getEntityTypeId(); + if ($entity->getEntityType()->isRevisionable()) { + $response_body['entity_revision_id'] = (string) $entity->getRevisionId(); + } + } + + $this->silverbackAiTokenUsage->createUsageEntry($response_body); + } +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilitiesInterface.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilitiesInterface.php new file mode 100644 index 000000000..d6c46863a --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/ImageAiUtilitiesInterface.php @@ -0,0 +1,159 @@ +<?php + +namespace Drupal\silverback_image_ai; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\file\FileInterface; +use Drupal\media\MediaInterface; + +/** + * @todo Add interface description. + */ +interface ImageAiUtilitiesInterface { + + /** + * Generates an ALT text for an image using the OpenAI API. + * + * This method takes an image file and a language code to generate + * a descriptive ALT text for the image. It utilizes the OpenAI API + * for ALT text generation if the API key is available. + * + * @param \Drupal\file\FileInterface $image + * The image file for which ALT text needs to be generated. + * @param string $langcode + * The language code representing the language in which the ALT text + * should be generated. Defaults to English if not specified or invalid. + * + * @return string|null + * The generated ALT text if successful, a message indicating a missing API key, + * or NULL if the API response does not contain the expected data. + * + * @todo + * Implement a fallback mechanism to return default ALT text in case of API failure. + */ + public function generateImageAlt(FileInterface $image, string $langcode); + + /** + * Converts an image file to a base64-encoded string. + * + * This method takes an image file represented by a FileInterface object, + * processes it through a specified image style to ensure the desired derivative + * is created, and then returns the image data encoded in base64 format, + * suitable for embedding in HTML. + * + * @param \Drupal\file\FileInterface $image + * The image file object for which the base64 data needs to be generated. + * + * @return string + * A string containing the base64-encoded image data prefixed with the + * appropriate data URI scheme and mime type. + * + * @todo + * Extract the image processing logic to a separate method for improved + * code maintainability and readability. + */ + public function getBase64EncodeData(FileInterface $image); + + /** + * Sends a request to the OpenAI API to generate ALT text for an image. + * + * This private method takes base64-encoded image data and a language code as parameters. + * It constructs a payload for the OpenAI API using the specified model and message format, + * including an instruction to generate a concise ALT text for the image in the specified language. + * + * @param string $base_64_data + * The base64-encoded data of the image for which to generate ALT text. + * @param string $langcode + * The language code for the language in which the ALT text should be generated. + * + * @return array + * The decoded JSON response from the OpenAI API containing the generated ALT text. + * + * @throws \Exception + * Thrown if the HTTP request to the OpenAI API fails. + */ + public function sendOpenAiRequest(string $base_64_data, string $langcode); + + /** + * Number of media entities with the 'image' bundle that are missing alt text. + * + * @return int + * The number of media entities missing alt text. + * + * @todo Create a db table to store data, query can be slow for large number of entities. + */ + public function getMissingAltEntitiesCount(); + + /** + * Sets the alt text for the media image field. + * + * This method updates the alt text of the given media entity's image field. + * It saves the changes to the entity unless the 'SILVERBACK_IMAGE_AI_DRY_RUN' environment + * variable is set. The method is intended for use with Drupal media entities. + * + * @param \Drupal\media\Entity\Media $media + * The media entity whose image alt text is being set. + * @param string $alt_text + * The alt text to set for the media image. + */ + public function setMediaImageAltText(MediaInterface $media, string $alt_text); + + /** + * Emulates a fake response. Used for development. + */ + public function getFakeResponseBody(string $base_64_data, string $langcode); + + /** + * Retrieves the total count of media items of type 'image'. + * + * This function executes a database query to count the distinct media items + * where the bundle is 'image' and the media ID (mid) is not null. + * + * @return int + * The total count of image media items. + */ + public function getMediaImagesTotalCount(); + + /** + * Gets a list of media entities. + * + * This function loads media entities of the 'image' bundle and iterates over + * their translations. It builds and returns an array of entities with language + * codes. + * + * @return array + * An array of arrays, each containing: + * - entity: The media entity translation. + * - langcode: The language code of the translation. + */ + public function getMediaEntitiesToUpdateAll(); + + /** + * Gets a list of media entities to update without alt value. + * + * This function loads media entities of the 'image' bundle and iterates over + * their translations. It builds and returns an array of entities with language + * codes. + * + * @return array + * An array of arrays, each containing: + * - entity: The media entity translation. + * - langcode: The language code of the translation. + */ + public function getMediaEntitiesToUpdateWithAlt(); + + /** + * Logs the usage of the Silverback Image AI module. + * + * This method updates the response body with module and entity details and + * creates a new usage entry using the Silverback AI Token Usage service. + * + * @param array $response_body + * An associative array that will be enhanced with module and entity information. + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * The entity for which to log usage details. If provided, its id, type, + * and revision id will be added to the response body if the entity is revisionable. + */ + public function logUsage(array $response_body, EntityInterface $entity = NULL); + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatch.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatch.php new file mode 100644 index 000000000..77ba7c885 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatch.php @@ -0,0 +1,134 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_image_ai; + +use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Logger\LoggerChannelInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Methods for running the ConfigImporter in a batch. + * + * @see \Drupal\Core\Config\ConfigImporter + */ +class MediaUpdaterBatch { + use StringTranslationTrait; + + /** + * The logger channel. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected LoggerChannelInterface $loggerChannel; + + /** + * Constructor. + * + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory + * The logger factory. + */ + public function __construct(LoggerChannelFactoryInterface $loggerFactory) { + $this->loggerChannel = $loggerFactory->get('silverback_image_ai'); + } + + /** + * Creates a batch operation to process media image updates. + * + * This method initializes a batch process for updating media images, setting + * up the batch operations and conditions for Drush integration if run via CLI. + * + * @param array $items + * An array of items to be processed in the batch. Each item represents + * a single media entity requiring updates. + * + * @return void + */ + public function create(array $items): void { + + $batchBuilder = (new BatchBuilder()) + ->setTitle($this->t('Running media image updates...')) + ->setFinishCallback([self::class, 'finish']) + ->setInitMessage('The initialization message (optional)') + ->setProgressMessage('Completed @current of @total. See other placeholders.'); + + $total = count($items); + $count = 0; + // Create multiple batch operations based on the $batchSize. + foreach ($items as $item) { + $batch = [ + 'item' => $item, + 'count' => $count++, + 'total' => $total, + ]; + $batchBuilder->addOperation([MediaUpdaterBatch::class, 'process'], [$batch]); + } + + batch_set($batchBuilder->toArray()); + if (function_exists('drush_backend_batch_process') && PHP_SAPI === 'cli') { + drush_backend_batch_process(); + } + } + + /** + * Batch operation callback. + * + * @param array $batch + * Information about batch (items, size, total, ...). + * @param array $context + * Batch context. + */ + public static function process(array $batch, array &$context) { + // Process elements stored in each batch (operation). + $processed = !empty($context['results']) ? count($context['results']) : $batch['count']; + $entity = $batch['item']['entity']; + + $service = \Drupal::service('silverback_image_ai.utilities'); + $alt_text = '-'; + $file = $entity->field_media_image->entity; + if ($file) { + $alt_text = $service->generateImageAlt($file, $batch['item']['langcode']); + $service->setMediaImageAltText($entity, $alt_text); + } + + $context['message'] = t('Processing media item @processed/@total with id: @id (@langcode) ', [ + '@processed' => $processed, + '@total' => $batch['total'], + '@id' => $entity->id(), + '@langcode' => $batch['item']['langcode'], + ]); + + sleep(1); + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + * + * @param bool $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of the operations that had not been completed by the batch API. + */ + public static function finish(bool $success, array $results, array $operations) { + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Items processed successfully.')); + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = t('An error occurred while processing %error_operation with arguments: @arguments', + ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]); + $messenger->addError($message); + } + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatchInterface.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatchInterface.php new file mode 100644 index 000000000..b33cf4fe6 --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/MediaUpdaterBatchInterface.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_image_ai; + +/** + * Methods for running the ConfigImporter in a batch. + * + * @see \Drupal\Core\Config\ConfigImporter + */ +interface MediaUpdaterBatchInterface { + + /** + * Creates a batch operation to process media image updates. + * + * This method initializes a batch process for updating media images, setting + * up the batch operations and conditions for Drush integration if run via CLI. + * + * @param array $items + * An array of items to be processed in the batch. Each item represents + * a single media entity requiring updates. + * + * @return void + */ + public function create(array $media_entities = []); + + /** + * Batch operation callback. + * + * @param array $batch + * Information about batch (items, size, total, ...). + * @param array $context + * Batch context. + */ + public static function process(array $batch, array &$context); + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + * + * @param bool $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of the operations that had not been completed by the batch API. + */ + public static function finish(bool $success, array $results, array $operations); + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Action/AltAiUpdateAction.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Action/AltAiUpdateAction.php new file mode 100644 index 000000000..c37a5112d --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Action/AltAiUpdateAction.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\silverback_image_ai\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Action\ActionBase; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Update ALT text for media images. + */ +#[Action( + id: 'entity:alt_ai_update_action', + action_label: new TranslatableMarkup('Alt text update (AI)'), + deriver: EntityPublishedActionDeriver::class +)] +class AltAiUpdateAction extends ActionBase { + + private const BUNDLE_IMAGE = 'image'; + private const DEFAULT_LANGCODE = 'en'; + + /** + * {@inheritdoc} + */ + public function execute($entity = NULL) { + // @todo Add DI + if ($entity->bundle() == self::BUNDLE_IMAGE) { + $langcode = $entity->langcode->value ?? self::DEFAULT_LANGCODE; + $service = \Drupal::service('silverback_image_ai.utilities'); + $file = $entity->field_media_image->entity; + if ($file) { + $alt_text = $service->generateImageAlt($file, $langcode); + if (!empty($alt_text)) { + $service->setMediaImageAltText($entity, $alt_text); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'create media'); + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/FocalPointImageWidgetAi.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/FocalPointImageWidgetAi.php new file mode 100644 index 000000000..e21350d3a --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/FocalPointImageWidgetAi.php @@ -0,0 +1,360 @@ +<?php + +namespace Drupal\silverback_image_ai\Plugin\Field\FieldWidget; + +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\crop\Entity\Crop; + +/** + * Plugin implementation of the 'image_focal_point' widget. + * + * @FieldWidget( + * id = "image_focal_point_ai", + * label = @Translation("Image AI (Focal Point)"), + * field_types = { + * "image" + * } + * ) + */ +class FocalPointImageWidgetAi extends ImageWidgetAi { + + const PREVIEW_TOKEN_NAME = 'focal_point_preview'; + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'progress_indicator' => 'throbber', + 'preview_image_style' => 'thumbnail', + 'preview_link' => TRUE, + 'offsets' => '50,50', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + // We need a preview image for this widget. + $form['preview_image_style']['#required'] = TRUE; + unset($form['preview_image_style']['#empty_option']); + // @todo Implement https://www.drupal.org/node/2872960 + // The preview image should not be generated using a focal point effect + // and should maintain the aspect ratio of the original image. + // phpcs:disable + $form['preview_image_style']['#description'] = t( + // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + $form['preview_image_style']['#description']->getUntranslatedString() . "<br/>Do not choose an image style that alters the aspect ratio of the original image nor an image style that uses a focal point effect.", + $form['preview_image_style']['#description']->getArguments(), + $form['preview_image_style']['#description']->getOptions() + ); + // phpcs:enable + + $form['preview_link'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Display preview link'), + '#default_value' => $this->getSetting('preview_link'), + '#weight' => 30, + ]; + + $form['offsets'] = [ + '#type' => 'textfield', + '#title' => $this->t('Default focal point value'), + '#default_value' => $this->getSetting('offsets'), + '#description' => $this->t('Specify the default focal point of this widget in the form "leftoffset,topoffset" where offsets are in percentages. Ex: 25,75.'), + '#size' => 7, + '#maxlength' => 7, + '#element_validate' => [[$this, 'validateFocalPointWidget']], + '#required' => TRUE, + '#weight' => 35, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $status = $this->getSetting('preview_link') ? $this->t('Yes') : $this->t('No'); + $summary[] = $this->t('Preview link: @status', ['@status' => $status]); + + $offsets = $this->getSetting('offsets'); + $summary[] = $this->t('Default focal point: @offsets', ['@offsets' => $offsets]); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + $element['#focal_point'] = [ + 'preview_link' => $this->getSetting('preview_link'), + 'offsets' => $this->getSetting('offsets'), + ]; + + return $element; + } + + /** + * {@inheritdoc} + * + * Processes an image_focal_point field Widget. + * + * Expands the image_focal_point Widget to include the focal_point field. + * This method is assigned as a #process callback in formElement() method. + * + * @todo Implement https://www.drupal.org/node/2657592 + * Convert focal point selector tool into a standalone form element. + * @todo Implement https://www.drupal.org/node/2848511 + * Focal Point offsets not accessible by keyboard. + */ + public static function process($element, FormStateInterface $form_state, $form) { + $element = parent::process($element, $form_state, $form); + + $item = $element['#value']; + $item['fids'] = $element['fids']['#value']; + $element_selectors = [ + 'focal_point' => 'focal-point-' . implode('-', $element['#parents']), + ]; + + $default_focal_point_value = $item['focal_point'] ?? $element['#focal_point']['offsets']; + + // Override the default Image Widget template when using the Media Library + // module so we can use the image field's preview rather than the preview + // provided by Media Library. + if ($form['#form_id'] == 'media_library_upload_form' || $form['#form_id'] == 'media_library_add_form') { + $element['#theme'] = 'focal_point_media_library_image_widget'; + unset($form['media'][0]['preview']); + } + + // Add the focal point indicator to preview. + if (isset($element['preview'])) { + $preview = [ + 'indicator' => self::createFocalPointIndicator($element['#delta'], $element_selectors), + 'thumbnail' => $element['preview'], + ]; + + // Even for image fields with a cardinality higher than 1 the correct fid + // can always be found in $item['fids'][0]. + $fid = $item['fids'][0] ?? ''; + if ($element['#focal_point']['preview_link'] && !empty($fid)) { + $preview['preview_link'] = self::createPreviewLink($fid, $element['#field_name'], $element_selectors, $default_focal_point_value); + } + + // Use the existing preview weight value so that the focal point indicator + // and thumbnail appear in the correct order. + $preview['#weight'] = $element['preview']['#weight'] ?? 0; + unset($preview['thumbnail']['#weight']); + + $element['preview'] = $preview; + } + + // Add the focal point field. + $element['focal_point'] = self::createFocalPointField($element['#field_name'], $element_selectors, $default_focal_point_value); + + return $element; + } + + /** + * {@inheritdoc} + * + * Form API callback. Retrieves the value for the file_generic field element. + * + * This method is assigned as a #value_callback in formElement() method. + */ + public static function value($element, $input, FormStateInterface $form_state) { + $return = parent::value($element, $input, $form_state); + + // When an element is loaded, focal_point needs to be set. During a form + // submission the value will already be there. + if (isset($return['target_id']) && !isset($return['focal_point'])) { + /** @var \Drupal\file\FileInterface $file */ + $file = \Drupal::service('entity_type.manager') + ->getStorage('file') + ->load($return['target_id']); + if ($file) { + $crop_type = \Drupal::config('focal_point.settings')->get('crop_type'); + $crop = Crop::findCrop($file->getFileUri(), $crop_type); + if ($crop) { + $anchor = \Drupal::service('focal_point.manager') + ->absoluteToRelative($crop->x->value, $crop->y->value, $return['width'], $return['height']); + $return['focal_point'] = "{$anchor['x']},{$anchor['y']}"; + } + } + else { + \Drupal::logger('focal_point')->notice("Attempted to get a focal point value for an invalid or temporary file."); + $return['focal_point'] = $element['#focal_point']['offsets']; + } + } + return $return; + } + + /** + * {@inheritdoc} + * + * Validation Callback; Focal Point process field. + */ + public static function validateFocalPoint($element, FormStateInterface $form_state) { + if (empty($element['#value']) || (FALSE === \Drupal::service('focal_point.manager')->validateFocalPoint($element['#value']))) { + $replacements = ['@title' => strtolower($element['#title'])]; + $form_state->setError($element, new TranslatableMarkup('The @title field should be in the form "leftoffset,topoffset" where offsets are in percentages. Ex: 25,75.', $replacements)); + } + } + + /** + * {@inheritdoc} + * + * Validation Callback; Focal Point widget setting. + */ + public function validateFocalPointWidget(array &$element, FormStateInterface $form_state) { + static::validateFocalPoint($element, $form_state); + } + + /** + * Create and return a token to use for accessing the preview page. + * + * @return string + * A valid token. + * + * @codeCoverageIgnore + */ + public static function getPreviewToken() { + return \Drupal::csrfToken()->get(self::PREVIEW_TOKEN_NAME); + } + + /** + * Validate a preview token. + * + * @param string $token + * A drupal generated token. + * + * @return bool + * True if the token is valid. + * + * @codeCoverageIgnore + */ + public static function validatePreviewToken($token) { + return \Drupal::csrfToken()->validate($token, self::PREVIEW_TOKEN_NAME); + } + + /** + * Create the focal point form element. + * + * @param string $field_name + * The name of the field element for the image field. + * @param array $element_selectors + * The element selectors to ultimately be used by javascript. + * @param string $default_focal_point_value + * The default focal point value in the form x,y. + * + * @return array + * The preview link form element. + */ + private static function createFocalPointField($field_name, array $element_selectors, $default_focal_point_value) { + $field = [ + '#type' => 'textfield', + '#title' => new TranslatableMarkup('Focal point'), + '#description' => new TranslatableMarkup('Specify the focus of this image in the form "leftoffset,topoffset" where offsets are in percents. Ex: 25,75'), + '#default_value' => $default_focal_point_value, + '#element_validate' => [[static::class, 'validateFocalPoint']], + '#attributes' => [ + 'class' => ['focal-point', $element_selectors['focal_point']], + 'data-selector' => $element_selectors['focal_point'], + 'data-field-name' => $field_name, + ], + '#wrapper_attributes' => [ + 'class' => ['focal-point-wrapper'], + ], + '#attached' => [ + 'library' => ['focal_point/drupal.focal_point'], + ], + ]; + + return $field; + } + + /** + * Create the focal point form element. + * + * @param int $delta + * The delta of the image field widget. + * @param array $element_selectors + * The element selectors to ultimately be used by javascript. + * + * @return array + * The focal point field form element. + */ + private static function createFocalPointIndicator($delta, array $element_selectors) { + $indicator = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'class' => ['focal-point-indicator'], + 'data-selector' => $element_selectors['focal_point'], + 'data-delta' => $delta, + ], + ]; + + return $indicator; + } + + /** + * Create the preview link form element. + * + * @param int $fid + * The fid of the image file. + * @param string $field_name + * The name of the field element for the image field. + * @param array $element_selectors + * The element selectors to ultimately be used by javascript. + * @param string $default_focal_point_value + * The default focal point value in the form x,y. + * + * @return array + * The preview link form element. + */ + private static function createPreviewLink($fid, $field_name, array $element_selectors, $default_focal_point_value) { + // Replace comma (,) with an x to make javascript handling easier. + $preview_focal_point_value = str_replace(',', 'x', $default_focal_point_value); + + // Create a token to be used during an access check on the preview page. + $token = self::getPreviewToken(); + + $preview_link = [ + '#type' => 'link', + '#title' => new TranslatableMarkup('Preview'), + '#url' => new Url('focal_point.preview', + [ + 'fid' => $fid, + 'focal_point_value' => $preview_focal_point_value, + ], + [ + 'query' => ['focal_point_token' => $token], + ]), + '#attached' => [ + 'library' => ['core/drupal.dialog.ajax'], + ], + '#attributes' => [ + 'class' => ['focal-point-preview-link', 'use-ajax'], + 'data-selector' => $element_selectors['focal_point'], + 'data-field-name' => $field_name, + 'data-dialog-type' => 'modal', + 'target' => '_blank', + ], + ]; + + return $preview_link; + } + +} diff --git a/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/ImageWidgetAi.php b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/ImageWidgetAi.php new file mode 100644 index 000000000..27c39e47a --- /dev/null +++ b/packages/drupal/silverback_ai/modules/silverback_image_ai/src/Plugin/Field/FieldWidget/ImageWidgetAi.php @@ -0,0 +1,454 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_image_ai\Plugin\Field\FieldWidget; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Image\ImageFactory; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\file\Entity\File; +use Drupal\file\Plugin\Field\FieldWidget\FileWidget; +use Drupal\image\Entity\ImageStyle; + +/** + * Defines the 'image_image_ai' field widget. + * + * @FieldWidget( + * id = "image_image_ai", + * label = @Translation("Image AI"), + * field_types = { + * "image" + * } + * ) + */ +class ImageWidgetAi extends FileWidget { + + private const DEFAULT_LANGCODE = 'en'; + + /** + * The image factory service. + * + * @var \Drupal\Core\Image\ImageFactory + */ + protected $imageFactory; + + /** + * Constructs an ImageWidgetAi object. + * + * @param string $plugin_id + * The plugin_id for the widget. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the widget is associated. + * @param array $settings + * The widget settings. + * @param array $third_party_settings + * Any third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info + * The element info manager service. + * @param \Drupal\Core\Image\ImageFactory $image_factory + * The image factory service. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info, ImageFactory $image_factory = NULL) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info); + $this->imageFactory = $image_factory ?: \Drupal::service('image.factory'); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'progress_indicator' => 'throbber', + 'preview_image_style' => 'thumbnail', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $element['preview_image_style'] = [ + '#title' => $this->t('Preview image style'), + '#type' => 'select', + '#options' => image_style_options(FALSE), + '#empty_option' => '<' . $this->t('no preview') . '>', + '#default_value' => $this->getSetting('preview_image_style'), + '#description' => $this->t('The preview image will be shown while editing the content.'), + '#weight' => 15, + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $image_styles = image_style_options(FALSE); + // Unset possible 'No defined styles' option. + unset($image_styles['']); + // Styles could be lost because of enabled/disabled modules that defines + // their styles in code. + $image_style_setting = $this->getSetting('preview_image_style'); + if (isset($image_styles[$image_style_setting])) { + $preview_image_style = $this->t('Preview image style: @style', ['@style' => $image_styles[$image_style_setting]]); + } + else { + $preview_image_style = $this->t('No preview'); + } + + array_unshift($summary, $preview_image_style); + + return $summary; + } + + /** + * Overrides \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElements(). + * + * Special handling for draggable multiple widgets and 'add more' button. + */ + protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) { + $elements = parent::formMultipleElements($items, $form, $form_state); + + $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + $file_upload_help = [ + '#theme' => 'file_upload_help', + '#description' => '', + '#upload_validators' => $elements[0]['#upload_validators'], + '#cardinality' => $cardinality, + ]; + if ($cardinality == 1) { + // If there's only one field, return it as delta 0. + if (empty($elements[0]['#default_value']['fids'])) { + $file_upload_help['#description'] = $this->getFilteredDescription(); + $elements[0]['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help); + } + } + else { + $elements['#file_upload_description'] = $file_upload_help; + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + $field_settings = $this->getFieldSettings(); + + // Add image validation. + $element['#upload_validators']['FileIsImage'] = []; + + // Add upload dimensions validation. + if ($field_settings['max_resolution'] || $field_settings['min_resolution']) { + $element['#upload_validators']['FileImageDimensions'] = [ + 'maxDimensions' => $field_settings['max_resolution'], + 'minDimensions' => $field_settings['min_resolution'], + ]; + } + + $extensions = $field_settings['file_extensions']; + $supported_extensions = $this->imageFactory->getSupportedExtensions(); + + // If using custom extension validation, ensure that the extensions are + // supported by the current image toolkit. Otherwise, validate against all + // toolkit supported extensions. + $extensions = !empty($extensions) ? array_intersect(explode(' ', $extensions), $supported_extensions) : $supported_extensions; + $element['#upload_validators']['FileExtension']['extensions'] = implode(' ', $extensions); + + // Add mobile device image capture acceptance. + $element['#accept'] = 'image/*'; + + // Add properties needed by process() method. + $element['#preview_image_style'] = $this->getSetting('preview_image_style'); + $element['#title_field'] = $field_settings['title_field']; + $element['#title_field_required'] = $field_settings['title_field_required']; + $element['#alt_field'] = $field_settings['alt_field']; + $element['#alt_field_required'] = $field_settings['alt_field_required']; + // Default image. + $default_image = $field_settings['default_image']; + if (empty($default_image['uuid'])) { + $default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image'); + } + // Convert the stored UUID into a file ID. + if (!empty($default_image['uuid']) && $entity = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid'])) { + $default_image['fid'] = $entity->id(); + } + $element['#default_image'] = !empty($default_image['fid']) ? $default_image : []; + return $element; + } + + /** + * Form API callback: Processes an image_image field element. + * + * Expands the image_image type to include the alt and title fields. + * + * This method is assigned as a #process callback in formElement() method. + */ + public static function process($element, FormStateInterface $form_state, $form) { + $item = $element['#value']; + + $item['fids'] = $element['fids']['#value']; + + $element['#theme'] = 'image_widget'; + // Add the image preview. + if (!empty($element['#files']) && $element['#preview_image_style']) { + $file = reset($element['#files']); + $variables = [ + 'style_name' => $element['#preview_image_style'], + 'uri' => $file->getFileUri(), + ]; + + $dimension_key = $variables['uri'] . '.image_preview_dimensions'; + // Determine image dimensions. + if (isset($element['#value']['width']) && isset($element['#value']['height'])) { + $variables['width'] = $element['#value']['width']; + $variables['height'] = $element['#value']['height']; + } + elseif ($form_state->has($dimension_key)) { + $variables += $form_state->get($dimension_key); + } + else { + $image = \Drupal::service('image.factory')->get($file->getFileUri()); + if ($image->isValid()) { + $variables['width'] = $image->getWidth(); + $variables['height'] = $image->getHeight(); + } + else { + $variables['width'] = $variables['height'] = NULL; + } + } + + $element['ai_container'] = [ + '#type' => 'container', + '#attributes' => [ + 'style' => ['text-align: right'], + ], + ]; + + $element['ai_container']['alt_ai_generate'] = [ + '#type' => 'submit', + '#value' => new TranslatableMarkup('Re-generate ALT text'), + '#weight' => -12, + '#attributes' => [ + 'class' => ['button--extrasmall', 'button', 'js-form-submit', 'form-submit'], + 'style' => ['padding: 2px 12px'], + ], + '#ajax' => [ + 'callback' => static::class . '::generateAjaxCallback', + 'event' => 'click', + 'wrapper_id' => $element['#attributes']['data-drupal-selector'] . '-alt', + 'fids' => $item['fids'], + 'langcode' => $form_state->get('langcode') ?? ImageWidgetAi::getLangcode($form, $form_state), + 'progress' => [ + 'type' => 'throbber', + 'message' => t('Generating alt text...'), + ], + ], + ]; + + // @todo Add DI + $disclaimer = "<div style='font-size: 0.8em'><em>" . \Drupal::config('silverback_image_ai.settings')->get('alt_disclaimer') . "</em></div>"; + $element['ai_container']['disclaimer'] = [ + '#type' => 'item', + '#markup' => Markup::create($disclaimer), + ]; + + $element['preview'] = [ + '#weight' => -10, + '#theme' => 'image_style', + '#width' => $variables['width'], + '#height' => $variables['height'], + '#style_name' => $variables['style_name'], + '#uri' => $variables['uri'], + ]; + + // Store the dimensions in the form so the file doesn't have to be + // accessed again. This is important for remote files. + $form_state->set($dimension_key, ['width' => $variables['width'], 'height' => $variables['height']]); + + // [AI utilities] + if (!isset($item['alt'])) { + $langcode = ImageWidgetAi::getLangcode($form, $form_state); + $service = \Drupal::service('silverback_image_ai.utilities'); + $item['alt'] = $service->generateImageAlt($file, $langcode ?? ImageWidgetAi::DEFAULT_LANGCODE); + } + // [end AI utilities] + } + elseif (!empty($element['#default_image'])) { + $default_image = $element['#default_image']; + $file = File::load($default_image['fid']); + if (!empty($file)) { + $element['preview'] = [ + '#weight' => -10, + '#theme' => 'image_style', + '#width' => $default_image['width'], + '#height' => $default_image['height'], + '#style_name' => $element['#preview_image_style'], + '#uri' => $file->getFileUri(), + ]; + } + } + + $element['alt'] = [ + '#title' => new TranslatableMarkup('Alternative text'), + '#type' => 'textfield', + '#default_value' => $item['alt'] ?? '', + '#description' => new TranslatableMarkup('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility and SEO.'), + // @see https://www.drupal.org/node/465106#alt-text + '#maxlength' => 512, + '#weight' => -12, + '#access' => (bool) $item['fids'] && $element['#alt_field'], + '#required' => $element['#alt_field_required'], + '#element_validate' => $element['#alt_field_required'] == 1 ? [[static::class, 'validateRequiredFields']] : [], + '#attributes' => [ + 'id' => [$element['#attributes']['data-drupal-selector'] . '-alt'], + ], + ]; + + $element['title'] = [ + '#type' => 'textfield', + '#title' => new TranslatableMarkup('Title'), + '#default_value' => $item['title'] ?? '', + '#description' => new TranslatableMarkup('The title is used as a tool tip when the user hovers the mouse over the image.'), + '#maxlength' => 1024, + '#weight' => -11, + '#access' => (bool) $item['fids'] && $element['#title_field'], + '#required' => $element['#title_field_required'], + '#element_validate' => $element['#title_field_required'] == 1 ? [[static::class, 'validateRequiredFields']] : [], + ]; + return parent::process($element, $form_state, $form); + } + + /** + * Validate callback for alt and title field, if the user wants them required. + * + * This is separated in a validate function instead of a #required flag to + * avoid being validated on the process callback. + */ + public static function validateRequiredFields($element, FormStateInterface $form_state) { + // Only do validation if the function is triggered from other places than + // the image process form. + $triggering_element = $form_state->getTriggeringElement(); + if (!empty($triggering_element['#submit']) && in_array('file_managed_file_submit', $triggering_element['#submit'], TRUE)) { + $form_state->setLimitValidationErrors([]); + } + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + $style_id = $this->getSetting('preview_image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + // If this widget uses a valid image style to display the preview of the + // uploaded image, add that image style configuration entity as dependency + // of this widget. + $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + $style_id = $this->getSetting('preview_image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) { + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage($style->getEntityTypeId()); + $replacement_id = $storage->getReplacementId($style_id); + // If a valid replacement has been provided in the storage, replace the + // preview image style with the replacement. + if ($replacement_id && ImageStyle::load($replacement_id)) { + $this->setSetting('preview_image_style', $replacement_id); + } + // If there's no replacement or the replacement is invalid, disable the + // image preview. + else { + $this->setSetting('preview_image_style', ''); + } + // Signal that the formatter plugin settings were updated. + $changed = TRUE; + } + } + return $changed; + } + + /** + * The textbox with the selected text. + */ + public static function generateAjaxCallback(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $wrapper_id = $triggering_element['#ajax']['wrapper_id']; + $fids = $triggering_element['#ajax']['fids']; + $langcode = $triggering_element['#ajax']['langcode'] ?? ImageWidgetAi::DEFAULT_LANGCODE; + // @todo get the file + $fid = reset($fids); + $file = NULL; + // $url = NULL; + if ($fid) { + $file = File::load($fid); + } + $response = new AjaxResponse(); + if ($file) { + $service = \Drupal::service('silverback_image_ai.utilities'); + $alt_text = $service->generateImageAlt($file, $langcode); + if ($alt_text) { + $response->addCommand(new InvokeCommand('#' . $wrapper_id, 'val', [$alt_text])); + } + } + return $response; + } + + /** + * + */ + public static function getLangcode(array &$form, FormStateInterface $form_state) { + // @todo Add DI + $langcode = ImageWidgetAi::DEFAULT_LANGCODE; + + $input = $form_state->getUserInput(); + $langcode = is_array($input['langcode']) ? reset($input['langcode']) : []; + $langcode = $langcode['value'] ?? NULL; + if (!empty($langcode)) { + $language_codes = \Drupal::languageManager()->getLanguages(); + // Make sure the selected langcode exists and it is a real language. + if (empty($language_codes[$langcode])) { + $langcode = ImageWidgetAi::DEFAULT_LANGCODE; + } + } + + if (!$langcode) { + // Try to fetch from entity language. + if ($prefixes = \Drupal::config('language.negotiation')->get('url.prefixes')) { + $language = \Drupal::languageManager()->getCurrentLanguage()->getId(); + $langcode = $prefixes[$language] ?? NULL; + } + } + + return $langcode; + } + +} diff --git a/packages/drupal/silverback_ai/silverback_ai.info.yml b/packages/drupal/silverback_ai/silverback_ai.info.yml new file mode 100644 index 000000000..e08d93929 --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.info.yml @@ -0,0 +1,7 @@ +name: 'Silverback AI' +type: module +description: 'Silverback AI base module' +package: Silverback +core_version_requirement: ^10 || ^11 +dependencies: + - webform:webform diff --git a/packages/drupal/silverback_ai/silverback_ai.install b/packages/drupal/silverback_ai/silverback_ai.install new file mode 100644 index 000000000..8f927c10c --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.install @@ -0,0 +1,127 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Silverback AI module. + */ + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Language\LanguageInterface; + +/** + * Implements hook_schema(). + */ +function silverback_ai_schema() { + + $db_schema = \Drupal::database()->schema(); + if ($db_schema->tableExists('silverback_ai_usage')) { + $db_schema->dropTable('silverback_ai_usage'); + } + + $schema['silverback_ai_usage'] = [ + 'description' => 'Usage for the Silverback AI module.', + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key.', + ], + 'uid' => [ + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user executed the ai fetch action.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'timestamp' => [ + 'description' => 'Date/time when the ai request, as Unix timestamp.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'langcode' => [ + 'description' => 'The language of this request.', + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + 'default' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + 'target_entity_type_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => FALSE, + 'default' => '', + 'description' => 'The ID of the associated entity type.', + ], + 'target_entity_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The ID of the associated entity.', + ], + 'target_entity_revision_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The revision ID of the associated entity.', + ], + 'tokens_in' => [ + 'description' => 'The total number of input tokens.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'big', + ], + 'tokens_out' => [ + 'description' => 'The total number of output tokens.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'big', + ], + 'total_count' => [ + 'description' => 'The total number of tokens used.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'big', + ], + 'provider' => [ + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The AI provider.', + ], + 'model' => [ + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The model used.', + ], + 'module' => [ + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The module used.', + ], + 'response' => [ + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'The response from the AI provider.', + ], + ], + 'primary key' => ['id'], + 'indexes' => [ + 'uid' => ['uid'], + 'timestamp' => ['timestamp'], + ], + ]; + + return $schema; +} diff --git a/packages/drupal/silverback_ai/silverback_ai.links.menu.yml b/packages/drupal/silverback_ai/silverback_ai.links.menu.yml new file mode 100644 index 000000000..5e424470d --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.links.menu.yml @@ -0,0 +1,17 @@ +silverback.ai.reports_usage: + title: 'Silverback AI usage' + parent: system.admin_reports + description: 'Overview of usage of Silverback AI plugins.' + route_name: silverback_ai.ai_usage +silverback_ai.admin_config_ai: + title: Silverback AI + parent: system.admin_config + description: 'Silverback AI settings.' + route_name: system.admin_config + weight: 0 +silverback_ai.ai_settings: + title: Silverback AI settings + description: Settings for the Silverback AI module. + parent: silverback_ai.admin_config_ai + route_name: silverback_ai.ai_settings + weight: 0 diff --git a/packages/drupal/silverback_ai/silverback_ai.module b/packages/drupal/silverback_ai/silverback_ai.module new file mode 100644 index 000000000..307ac1ba2 --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.module @@ -0,0 +1,28 @@ +<?php + +/** + * @file + * Primary module hooks for Silverback AI module. + */ + +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Implements hook_help(). + */ +function silverback_ai_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.silverback_ai': + $output = ''; + $output .= '<h2>' . t('About') . '</h2>'; + $output .= '<p>' . t('..'); + $output .= '<h2>' . t('Uses') . '</h2>'; + $output .= '<dl>'; + $output .= '<dt>' . t('Monitoring tokens usage') . '</dt>'; + $output .= '</dl>'; + return $output; + + case 'silverback_ai.overview': + return '<p>' . t('The Silverback AI module provides ...') . '</p>'; + } +} diff --git a/packages/drupal/silverback_ai/silverback_ai.permissions.yml b/packages/drupal/silverback_ai/silverback_ai.permissions.yml new file mode 100644 index 000000000..d5d99df0c --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.permissions.yml @@ -0,0 +1,3 @@ +access token usage: + title: 'Access token usage' + description: 'Allows a user to access the site AI services token usage.' diff --git a/packages/drupal/silverback_ai/silverback_ai.routing.yml b/packages/drupal/silverback_ai/silverback_ai.routing.yml new file mode 100644 index 000000000..9c13da26c --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.routing.yml @@ -0,0 +1,23 @@ +silverback_ai.ai_usage: + path: '/admin/reports/silverback-ai-usage' + defaults: + _title: 'Silverback AI usage' + _controller: '\Drupal\silverback_ai\Controller\AiUsageController' + requirements: + _permission: 'access token usage' + +silverback_ai.ai_usage.details: + path: '/admin/reports/silverback-ai-usage/{record}/details' + defaults: + _title: 'Silverback AI usage details' + _controller: '\Drupal\silverback_ai\Controller\UsageDetailsController' + requirements: + _permission: 'access token usage' + +silverback_ai.ai_settings: + path: '/admin/config/system/silverback-ai-settings' + defaults: + _title: 'Silverback AI settings' + _form: 'Drupal\silverback_ai\Form\SilverbackAiSettingsForm' + requirements: + _permission: 'administer site configuration' diff --git a/packages/drupal/silverback_ai/silverback_ai.services.yml b/packages/drupal/silverback_ai/silverback_ai.services.yml new file mode 100644 index 000000000..3ed140758 --- /dev/null +++ b/packages/drupal/silverback_ai/silverback_ai.services.yml @@ -0,0 +1,10 @@ +services: + silverback_ai.token.usage: + class: Drupal\silverback_ai\TokenUsage + arguments: ['@database', '@current_user', '@logger.factory', '@config.factory', '@entity_type.manager'] + silverback_ai.openai_http_client: + class: Drupal\silverback_ai\HttpClient\OpenAiHttpClient + arguments: ['@http_client_factory', '@config.factory'] + silverback_ai.service: + class: Drupal\silverback_ai\AiService + arguments: ['@current_route_match', '@current_user', '@entity_type.manager', '@logger.factory', '@config.factory', '@silverback_ai.openai_http_client','@silverback_ai.token.usage'] diff --git a/packages/drupal/silverback_ai/src/AiService.php b/packages/drupal/silverback_ai/src/AiService.php new file mode 100644 index 000000000..69628f8e2 --- /dev/null +++ b/packages/drupal/silverback_ai/src/AiService.php @@ -0,0 +1,124 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\silverback_ai\HttpClient\OpenAiHttpClient; +use Drupal\silverback_ai\TokenUsage; +use GuzzleHttp\Exception\RequestException; + +/** + * Contains general AI services, such as a general request method and more. + */ +final class AiService { + + private const DEFAULT_AI_MODEL = 'gpt-4o-mini'; + + /** + * Constructs a service object. + */ + public function __construct( + private readonly RouteMatchInterface $routeMatch, + private readonly AccountProxyInterface $currentUser, + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly LoggerChannelFactoryInterface $loggerFactory, + private readonly ConfigFactoryInterface $configFactory, + private readonly OpenAiHttpClient $silverbackAiOpenaiHttpClient, + private readonly TokenUsage $tokenUsage, + ) { + } + + /** + * Makes a chat completion request to the OpenAI API. + * + * @param string $prompt The text prompt to send to the AI model + * @param string $model The AI model to use for the request (defaults to class constant DEFAULT_AI_MODEL) + * @param array $context Additional context data for logging purposes + * + * @return array The decoded JSON response from the API + * + * @throws \Exception If the HTTP request fails or JSON decoding fails + */ + public function request(string $prompt, string $model = self::DEFAULT_AI_MODEL, array $context = []) { + + $payload = [ + 'model' => $model, + 'messages' => [ + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => $prompt, + ], + ], + ], + ], + ]; + + try { + $response = $this->silverbackAiOpenaiHttpClient->post('chat/completions', [ + 'json' => $payload, + ]); + } catch (\Exception $e) { + throw new \Exception('HTTP request failed: ' . $e->getMessage()); + } + + $responseBodyContents = $response->getBody()->getContents(); + $response = json_decode($responseBodyContents, TRUE, 512, JSON_THROW_ON_ERROR); + $this->logUsage($response, $context); + + return $response; + } + + /** + * List OpenAI available models. + * + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function listModels() { + try { + $response = $this->silverbackAiOpenaiHttpClient->get('models'); + $responseBodyContents = $response->getBody()->getContents(); + $response = json_decode($responseBodyContents, TRUE, 512, JSON_THROW_ON_ERROR); + return $response['data']; + } catch (\Exception $e) { + throw new \Exception('HTTP request failed: ' . $e->getMessage()); + } + return []; + } + + /** + * Logs the tokens usage. + * + * This method updates the response body with module and entity details and + * creates a new usage entry using the Silverback AI Token Usage service. + * + * @param array $response_body + * An associative array that will be enhanced with module and entity information. + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * The entity for which to log usage details. If provided, its id, type, + * and revision id will be added to the response body if the entity is revisionable. + * + * @throws \Exception + */ + private function logUsage(array $response_body, array $context) { + $response_body['module'] = $context['module'] ?? 'silverback_ai'; + if (isset($context['entity'])) { + $entity = $context['entity']; + $response_body['entity_id'] = (string) $entity->id(); + $response_body['entity_type_id'] = (string) $entity->getEntityTypeId(); + if ($entity->getEntityType()->isRevisionable()) { + $response_body['entity_revision_id'] = (string) $entity->getRevisionId(); + } + } + $this->tokenUsage->createUsageEntry($response_body); + } +} diff --git a/packages/drupal/silverback_ai/src/Controller/AiUsageController.php b/packages/drupal/silverback_ai/src/Controller/AiUsageController.php new file mode 100644 index 000000000..4122a05f5 --- /dev/null +++ b/packages/drupal/silverback_ai/src/Controller/AiUsageController.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\silverback_ai\TokenUsageInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Returns responses for Silverback AI routes. + */ +class AiUsageController extends ControllerBase { + + use StringTranslationTrait; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The token usage service. + * + * @var \Drupal\silverback_ai\TokenUsage + */ + protected $silverbackAiTokenUsage; + + /** + * The account object. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The controller constructor. + */ + public function __construct( + EntityTypeManagerInterface $entityTypeManager, + TokenUsageInterface $silverbackAiTokenUsage, + AccountProxyInterface $currentUser, + ) {} + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container): self { + return new self( + $container->get('entity_type.manager'), + $container->get('silverback_ai.token.usage'), + $container->get('current_user'), + ); + } + + /** + * Builds the response. + */ + public function __invoke(): array { + + $header = [ + 'timestamp' => $this->t('Timestamp'), + 'username' => $this->t('User'), + 'entity_id' => $this->t('Entity type'), + 'tokens_total' => $this->t('Tokens used'), + 'ai_provider' => $this->t('Provider / Model'), + 'module_name' => $this->t('Module'), + 'info' => $this->t('Information'), + ]; + + // @todo Add DI + $entries = \Drupal::service('silverback_ai.token.usage')->getEntries(); + $entries = array_map(function ($item) { + unset($item['response']); + return $item; + }, $entries); + + $build['table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $entries, + '#sticky' => TRUE, + '#empty' => $this->t('No records found'), + ]; + + $build['pager'] = [ + '#type' => 'pager', + ]; + + return $build; + } + +} diff --git a/packages/drupal/silverback_ai/src/Controller/UsageDetailsController.php b/packages/drupal/silverback_ai/src/Controller/UsageDetailsController.php new file mode 100644 index 000000000..a261977a5 --- /dev/null +++ b/packages/drupal/silverback_ai/src/Controller/UsageDetailsController.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Database\Connection; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Serialization\Yaml; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\silverback_ai\TokenUsage; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Returns responses for Silverback AI routes. + */ +class UsageDetailsController extends ControllerBase { + + use StringTranslationTrait; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * The token usage service. + * + * @var \Drupal\silverback_ai\TokenUsage + */ + protected $tokenUsage; + + /** + * The controller constructor. + */ + public function __construct(Connection $connection, TokenUsage $token_usage) { + $this->connection = $connection; + $this->tokenUsage = $token_usage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container): self { + return new self( + $container->get('database'), + $container->get('silverback_ai.token.usage'), + ); + } + + /** + * Generates an overview table of revisions for an entity. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch + * The route match. + * + * @return array + * A render array. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function __invoke(RouteMatchInterface $routeMatch): array { + + $id = $routeMatch->getParameter('record'); + + $query = $this->connection->select('silverback_ai_usage', 's') + ->condition('s.id', $id) + ->fields('s', [ + 'id', + 'uid', + 'timestamp', + 'target_entity_id', + 'target_entity_type_id', + 'target_entity_revision_id', + 'tokens_in', + 'tokens_out', + 'total_count', + 'provider', + 'model', + 'module', + 'response', + ]); + $records = $query->execute(); + foreach ($records->fetchAll() as $row) { + $info = $this->tokenUsage->buildRow($row); + $build['render_array'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Response details'), + 'source' => [ + '#theme' => 'webform_codemirror', + '#type' => 'yaml', + '#code' => Yaml::encode($info['response']), + ], + ]; + + } + + return $build; + } + +} diff --git a/packages/drupal/silverback_ai/src/Form/SilverbackAiSettingsForm.php b/packages/drupal/silverback_ai/src/Form/SilverbackAiSettingsForm.php new file mode 100644 index 000000000..7cdd3b1cb --- /dev/null +++ b/packages/drupal/silverback_ai/src/Form/SilverbackAiSettingsForm.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Configure Silverback Alt AI settings for this site. + */ +final class SilverbackAiSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'silverback_ai_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['silverback_ai.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + $form['credentials'] = [ + '#type' => 'details', + '#title' => $this->t('Open AI credentials'), + '#open' => TRUE, + ]; + + $form['credentials']['open_ai_base_uri'] = [ + '#type' => 'textfield', + '#title' => $this->t('Base URI'), + '#default_value' => $this->t('https://api.openai.com/v1/'), + '#description' => $this->t("The OPEN AI API endpoint.") , + ]; + + // Try to fetch ket from open ai module. + $api_key = $this->config('openai.settings')->get('api_key'); + $api_org = $this->config('openai.settings')->get('api_org'); + + $form['credentials']['open_ai_key'] = [ + '#type' => 'password', + '#title' => $this->t('Open AI key'), + '#description' => $this->t("The OPEN AI key for this project.") . '<br />' . + $this->t('Install the <strong><a href="@href" target="_blank">Open AI</a></strong> module to use the defined key from the module settings.', [ + '@href' => 'https://www.drupal.org/project/openai', + ]), + '#default_value' => $this->config('silverback_ai.settings')->get('open_ai_api_key'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('silverback_ai.settings') + ->set('open_ai_base_uri', $form_state->getValue('open_ai_base_uri')) + ->set('open_ai_api_key', $form_state->getValue('open_ai_api_key')) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/packages/drupal/silverback_ai/src/HttpClient/OpenAiHttpClient.php b/packages/drupal/silverback_ai/src/HttpClient/OpenAiHttpClient.php new file mode 100644 index 000000000..948793c0f --- /dev/null +++ b/packages/drupal/silverback_ai/src/HttpClient/OpenAiHttpClient.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\silverback_ai\HttpClient; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Http\ClientFactory; +use GuzzleHttp\Client; + +/** + * Custom HTTP client for OpenAI API. + */ +class OpenAiHttpClient extends Client { + + /** + * Constructs a new OpenAiHttpClient object. + * + * @param \Drupal\Core\Http\ClientFactory $http_client_factory + * The HTTP client factory. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct(ClientFactory $http_client_factory, ConfigFactoryInterface $config_factory) { + $config = $config_factory->get('silverback_ai.settings'); + $open_ai_api_key = $config->get('open_ai_api_key') ?? ''; + $open_ai_base_uri = $config->get('open_ai_base_uri') ?: 'https://api.openai.com/v1/'; + + $options = [ + 'base_uri' => $open_ai_base_uri, + 'headers' => [ + 'Authorization' => 'Bearer ' . $open_ai_api_key, + 'Content-Type' => 'application/json', + ], + ]; + + parent::__construct($options); + } +} diff --git a/packages/drupal/silverback_ai/src/TokenUsage.php b/packages/drupal/silverback_ai/src/TokenUsage.php new file mode 100644 index 000000000..6111b0115 --- /dev/null +++ b/packages/drupal/silverback_ai/src/TokenUsage.php @@ -0,0 +1,199 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Link; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\user\Entity\User; + +/** + * @todo Add class description. + */ +final class TokenUsage implements TokenUsageInterface { + + private const USER_ADMIN = 1; + private const PAGER_LIMIT = 25; + + use StringTranslationTrait; + + /** + * Constructs a TokenUsage object. + */ + public function __construct( + private readonly Connection $connection, + private readonly AccountProxyInterface $currentUser, + private readonly LoggerChannelFactoryInterface $loggerFactory, + private readonly ConfigFactoryInterface $configFactory, + private readonly EntityTypeManagerInterface $entityTypeManager, + ) { + } + + /** + * {@inheritdoc} + */ + public function createUsageEntry(array $context): void { + + $tokens_out = $context['usage']['prompt_tokens']; + $tokens_in = $context['usage']['completion_tokens']; + $tokens_total = $context['usage']['total_tokens']; + + $uid = self::USER_ADMIN; + if ($this->currentUser) { + $uid = $this->currentUser->id(); + } + + // @todo Validate input array + try { + $this->connection + ->insert('silverback_ai_usage') + ->fields([ + 'uid' => $uid, + 'timestamp' => (new DrupalDateTime())->getTimestamp(), + 'target_entity_type_id' => $context['entity_type_id'] ?? NULL, + 'target_entity_id' => $context['entity_id'] ?? NULL, + 'target_entity_revision_id' => $context['entity_revision_id'] ?? NULL, + 'tokens_in' => $tokens_in, + 'tokens_out' => $tokens_out, + 'total_count' => $tokens_total, + 'provider' => 'Open AI', + 'model' => $context['model'], + 'module' => $context['module'], + 'response' => json_encode($context), + ]) + ->execute(); + } + catch (\Exception $e) { + $this->loggerFactory->get('silverback_ai')->error($e->getMessage()); + } + } + + /** + * Retrieves a list of entries from the 'silverback_ai_usage' table. + * + * This function queries the database to select fields related to AI usage + * and orders them by ID in descending order. It paginates the result + * according to a predefined limit. Each row fetched from the database + * is processed using the `buildRow` method before being added to the result set. + * + * @return array + * An array of processed database records. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getEntries() { + $query = $this->connection->select('silverback_ai_usage', 's') + ->fields('s', [ + 'id', + 'uid', + 'timestamp', + 'target_entity_id', + 'target_entity_type_id', + 'target_entity_revision_id', + 'tokens_in', + 'tokens_out', + 'total_count', + 'provider', + 'model', + 'module', + 'response', + ]) + ->orderBy('id', 'DESC'); + $pager = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit(self::PAGER_LIMIT); + $rsc = $pager->execute(); + $rows = []; + + foreach ($rsc->fetchAll() as $row) { + $rows[] = $this->buildRow($row); + } + return $rows; + } + + /** + * Builds a renderable array representing a row of data. + * + * This method constructs an array of information based on the data from + * the provided row, including entity details, user information, and additional + * metadata such as timestamps and provider information. + * + * @param object $row + * The data row object containing properties such as 'target_entity_id', + * 'target_entity_type_id', 'uid', 'timestamp', 'total_count', 'provider', + * 'model', and 'module'. + * + * @return array + * A renderable array with the following elements: + * - 'timestamp': The formatted timestamp of when the entry was created. + * - 'username': The display name of the user associated with the entry. + * - 'entity_id': The capitalized entity bundle string or empty string if + * the entity is not found. + * - 'tokens_total': The total token count from the row's data. + * - 'ai_provider': A string indicating the AI provider and model used. + * - 'module_name': The name of the module associated with the entry. + * - 'info': A renderable link to detailed usage information displayed in + * a modal dialog. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function buildRow($row) { + $entity_info = ''; + if ($row->target_entity_id && $row->target_entity_type_id) { + // @todo Aldo check revision + $entity = $this->entityTypeManager->getStorage($row->target_entity_type_id)->load($row->target_entity_id); + $entity_info = $entity ? $entity->bundle() : ''; + // @todo Add url to entity. Problem is the e.g. File entities + // they return exception calling this method. + } + + $user = User::load($row->uid); + $username = ''; + if ($user) { + $username = $user->getDisplayName(); + } + + $icon_info = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> + <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/> + </svg>'; + + $link = Link::createFromRoute( + Markup::create($icon_info), + 'silverback_ai.ai_usage.details', + ['record' => $row->id], + [ + 'attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 800, + ]), + ], + 'attached' => [ + 'library' => ['core/drupal.dialog.ajax'], + ], + ] + ); + + return [ + 'timestamp' => DrupalDateTime::createFromTimestamp($row->timestamp)->format('d.m.Y H:i'), + 'username' => $username, + 'entity_id' => ucfirst($entity_info), + 'tokens_total' => $row->total_count, + 'ai_provider' => $row->provider . ' / ' . ($row->model ?: 'gpt-4o-mini'), + 'module_name' => $row->module, + 'info' => $link, + 'response' => $row->response, + ]; + } + +} diff --git a/packages/drupal/silverback_ai/src/TokenUsageInterface.php b/packages/drupal/silverback_ai/src/TokenUsageInterface.php new file mode 100644 index 000000000..1ff6bc258 --- /dev/null +++ b/packages/drupal/silverback_ai/src/TokenUsageInterface.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\silverback_ai; + +/** + * @todo Interface for the token usage service. + */ +interface TokenUsageInterface { + + /** + * Creates a usage entry in the database. + * + * This function attempts to insert a new entry in the 'silverback_ai_usage' table + * with the provided context information. It records the current user's ID, the + * current timestamp, and various fields from the context array. The mode is + * retrieved from the configuration settings for 'silverback_image_ai'. + * + * @param array $context + * An associative array containing context keys: + * - 'tokens_in': The number of input tokens. + * - 'tokens_out': The number of output tokens. + * - 'totalcount': The total count associated with the entry. + * + * @throws \Exception If there is an error during the database insertion. + * + * @todo Validate the input array to ensure required keys are present and values are valid. + * @todo Handle exceptions thrown during the insertion process. + */ + public function createUsageEntry(array $context); + +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cad50f43..02cc73353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,75 @@ importers: specifier: workspace:* version: link:../../packages/ui + apps/converter: + dependencies: + '@extractus/article-extractor': + specifier: ^8.0.16 + version: 8.0.16 + '@opendocsg/pdf2md': + specifier: ^0.2.1 + version: 0.2.1 + '@textlint/markdown-to-ast': + specifier: ^14.3.0 + version: 14.4.2 + axios: + specifier: ^1.7.9 + version: 1.7.9 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + express: + specifier: ^4.21.1 + version: 4.21.2 + fs-extra: + specifier: ^11.2.0 + version: 11.2.0 + hast-util-to-html: + specifier: ^9.0.3 + version: 9.0.4 + image-type: + specifier: ^5.2.0 + version: 5.2.0 + install: + specifier: ^0.13.0 + version: 0.13.0 + jsdom: + specifier: ^25.0.1 + version: 25.0.1 + mammoth: + specifier: ^1.8.0 + version: 1.9.0 + markdownlint: + specifier: ^0.37.1 + version: 0.37.3 + mdast-util-from-markdown: + specifier: ^2.0.2 + version: 2.0.2 + mdast-util-to-hast: + specifier: ^13.2.0 + version: 13.2.0 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + openai: + specifier: ^4.77.0 + version: 4.78.1 + pdf-parse: + specifier: github:iamh2o/pdf-parse#1.1.3 + version: github.com/iamh2o/pdf-parse/d7a41d5aaed1503bee2d7ea50bf89588d3b2d2cf + pdf2pic: + specifier: ^3.1.3 + version: 3.1.3 + sanitize-filename: + specifier: ^1.6.3 + version: 1.6.3 + turndown: + specifier: ^7.2.0 + version: 7.2.0 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + apps/decap: dependencies: '@amazeelabs/cloudinary-responsive-image': @@ -1449,6 +1518,16 @@ packages: transitivePeerDependencies: - encoding + /@asamuzakjp/css-color@2.8.2: + resolution: {integrity: sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==} + dependencies: + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4)(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4)(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 11.0.2 + dev: false + /@babel/code-frame@7.12.11: resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} requiresBuild: true @@ -3006,6 +3085,49 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: false + /@csstools/color-helpers@5.0.1: + resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + engines: {node: '>=18'} + dev: false + + /@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4)(@csstools/css-tokenizer@3.0.3): + resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + dev: false + + /@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4)(@csstools/css-tokenizer@3.0.3): + resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + dependencies: + '@csstools/color-helpers': 5.0.1 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4)(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + dev: false + + /@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3): + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + dependencies: + '@csstools/css-tokenizer': 3.0.3 + dev: false + + /@csstools/css-tokenizer@3.0.3: + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + dev: false + /@dabh/diagnostics@2.0.3: resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} dependencies: @@ -4164,6 +4286,19 @@ packages: levn: 0.4.1 dev: true + /@extractus/article-extractor@8.0.16: + resolution: {integrity: sha512-amxCKO2uerY0UPxDVSoTDdcTny0otpKsAIGC2q2CUDEhUX6EfxmpURttlKLx9uWFT9DRlNX9LSyMSP/2p7kFLg==} + engines: {node: '>= 18'} + dependencies: + '@mozilla/readability': 0.5.0 + bellajs: 11.2.0 + cross-fetch: 4.1.0 + linkedom: 0.18.6 + sanitize-html: 2.13.1 + transitivePeerDependencies: + - encoding + dev: false + /@fastify/accept-negotiator@1.1.0: resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} engines: {node: '>=14'} @@ -5992,6 +6127,7 @@ packages: /@mapbox/node-pre-gyp@1.0.11: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + requiresBuild: true dependencies: detect-libc: 2.0.3 https-proxy-agent: 5.0.1 @@ -6052,6 +6188,10 @@ packages: '@lezer/lr': 1.4.0 json5: 2.2.3 + /@mixmark-io/domino@2.2.0: + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + dev: false + /@mole-inc/bin-wrapper@8.0.1: resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6065,6 +6205,11 @@ packages: got: 11.8.6 os-filter-obj: 2.0.0 + /@mozilla/readability@0.5.0: + resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==} + engines: {node: '>=14.0.0'} + dev: false + /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} cpu: [arm64] @@ -6838,6 +6983,18 @@ packages: '@octokit/openapi-types': 18.1.1 dev: false + /@opendocsg/pdf2md@0.2.1: + resolution: {integrity: sha512-k/yvfrTb+GPTIIm/bMm5IsenTqAFl+IqvkBgFwFlmflS5TT7FOfyRLp8MypVWLAG4G9AnT7AZFbdQYgN/CR5BA==} + hasBin: true + dependencies: + enumify: 1.0.4 + minimist: 1.2.8 + unpdf: 0.12.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@opentelemetry/api@1.8.0: resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} engines: {node: '>=8.0.0'} @@ -8818,6 +8975,26 @@ packages: '@testing-library/dom': 10.4.0 dev: true + /@textlint/ast-node-types@14.4.2: + resolution: {integrity: sha512-e8/drNznaZHS/qGDC83k6Ht1wDWNHzGQ0RHcXD+72YMFercEFvp6WVfW5XbCbxGbSITEO5NBCOCTyeccS9lxEA==} + dev: false + + /@textlint/markdown-to-ast@14.4.2: + resolution: {integrity: sha512-hj2xR9hz5/Tu7Hlrn6VORJgdAfUhAd5j6cBkEVpnKAU4LaERkNyVCgK/da2JHK2w84YHmaDjER4D6zUUkllwag==} + dependencies: + '@textlint/ast-node-types': 14.4.2 + debug: 4.4.0 + mdast-util-gfm-autolink-literal: 0.1.3 + neotraverse: 0.6.18 + remark-footnotes: 3.0.0 + remark-frontmatter: 3.0.0 + remark-gfm: 1.0.0 + remark-parse: 9.0.0 + unified: 9.2.2 + transitivePeerDependencies: + - supports-color + dev: false + /@tokenizer/token@0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -9084,7 +9261,6 @@ packages: resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} dependencies: '@types/unist': 3.0.2 - dev: true /@types/hoist-non-react-statics@3.3.5: resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -9155,6 +9331,10 @@ packages: resolution: {integrity: sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==} dev: true + /@types/katex@0.16.7: + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + dev: false + /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -9173,7 +9353,6 @@ packages: resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} dependencies: '@types/unist': 3.0.2 - dev: true /@types/mdx@2.0.13: resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -9211,13 +9390,19 @@ packages: /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: - '@types/node': 20.11.17 + '@types/node': 22.7.2 form-data: 4.0.0 /@types/node@17.0.45: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} dev: false + /@types/node@18.19.70: + resolution: {integrity: sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.11.17: resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==} dependencies: @@ -9227,7 +9412,6 @@ packages: resolution: {integrity: sha512-866lXSrpGpgyHBZUa2m9YNWqHDjjM0aBTJlNtYaGEw4rqY/dcD7deRVTbBBAJelfA7oaGDbNftXF/TL/A6RgoA==} dependencies: undici-types: 6.19.8 - dev: true /@types/node@8.10.66: resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} @@ -9404,7 +9588,6 @@ packages: /@types/unist@3.0.2: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - dev: true /@types/uuid@9.0.8: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -10985,6 +11168,11 @@ packages: p-event: 5.0.1 dev: false + /@xmldom/xmldom@0.8.10: + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + dev: false + /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -11124,6 +11312,7 @@ packages: /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + requiresBuild: true dependencies: debug: 4.3.7 transitivePeerDependencies: @@ -11146,7 +11335,11 @@ packages: debug: 4.3.7 transitivePeerDependencies: - supports-color - dev: true + + /agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + dev: false /agentkeepalive@4.5.0: resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} @@ -11155,7 +11348,6 @@ packages: dependencies: humanize-ms: 1.2.1 dev: false - optional: true /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -11314,6 +11506,7 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + requiresBuild: true /ansi-regex@6.1.0: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} @@ -11462,6 +11655,7 @@ packages: /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + requiresBuild: true dev: false /arch@2.2.0: @@ -11499,6 +11693,7 @@ packages: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} deprecated: This package is no longer supported. + requiresBuild: true dependencies: delegates: 1.0.0 readable-stream: 3.6.2 @@ -11586,6 +11781,14 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false + /array-parallel@0.1.3: + resolution: {integrity: sha512-TDPTwSWW5E4oiFiKmz6RGJ/a80Y91GuLgUYuLd49+XBS75tYo8PNgaT2K/OxuQYqkoI852MDGBorg9OcUSTQ8w==} + dev: false + + /array-series@0.1.5: + resolution: {integrity: sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==} + dev: false + /array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} dev: false @@ -11920,6 +12123,16 @@ packages: transitivePeerDependencies: - debug + /axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -12220,6 +12433,7 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + requiresBuild: true /bare-events@2.2.2: resolution: {integrity: sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==} @@ -12266,6 +12480,11 @@ packages: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} dev: false + /bellajs@11.2.0: + resolution: {integrity: sha512-Wjss+Bc674ZABPr+SCKWTqA4V1pyYFhzDTjNBJy4jdmgOv0oGIGXeKBRJyINwP5tIy+iIZD9SfgZpztduzQ5QA==} + engines: {node: '>= 18.4'} + dev: false + /better-ajv-errors@1.2.0(ajv@8.12.0): resolution: {integrity: sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==} engines: {node: '>= 12.13.0'} @@ -12342,6 +12561,10 @@ packages: readable-stream: 3.6.2 dev: false + /bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + dev: false + /bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -12387,6 +12610,26 @@ packages: transitivePeerDependencies: - supports-color + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -12437,6 +12680,7 @@ packages: /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + requiresBuild: true dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -12719,6 +12963,20 @@ packages: /caniuse-lite@1.0.30001608: resolution: {integrity: sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==} + /canvas@2.11.2: + resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + nan: 2.19.0 + simple-get: 3.1.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: @@ -12885,6 +13143,10 @@ packages: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} dev: false + /character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + dev: false + /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -12962,6 +13224,7 @@ packages: /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + requiresBuild: true dev: false /chrome-trace-event@1.0.3: @@ -13219,6 +13482,7 @@ packages: /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + requiresBuild: true dev: false /color@3.2.1: @@ -13315,6 +13579,11 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -13396,6 +13665,7 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + requiresBuild: true /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -13474,6 +13744,7 @@ packages: /console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + requiresBuild: true dev: false /consolidated-events@2.0.2: @@ -13573,6 +13844,11 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + /copy-descriptor@0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} @@ -13788,12 +14064,27 @@ packages: transitivePeerDependencies: - encoding + /cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-inspect@1.0.0: resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} engines: {node: '>=16.0.0'} dependencies: tslib: 2.6.2 + /cross-spawn@4.0.2: + resolution: {integrity: sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==} + dependencies: + lru-cache: 4.1.5 + which: 1.3.1 + dev: false + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -14092,6 +14383,18 @@ packages: dependencies: css-tree: 2.2.1 + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: false + + /cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + dependencies: + '@asamuzakjp/css-color': 2.8.2 + rrweb-cssom: 0.8.0 + dev: false + /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -14127,6 +14430,14 @@ packages: engines: {node: '>= 12'} dev: false + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + dev: false + /data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -14235,6 +14546,18 @@ packages: supports-color: 9.4.0 dev: false + /debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + /decache@4.6.2: resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} dependencies: @@ -14974,6 +15297,10 @@ packages: - '@types/react' dev: false + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: @@ -14988,6 +15315,15 @@ packages: engines: {node: '>=14.16'} dev: false + /decompress-response@4.2.1: + resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + mimic-response: 2.1.0 + dev: false + optional: true + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -15140,6 +15476,7 @@ packages: /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + requiresBuild: true dev: false /depd@1.1.2: @@ -15329,7 +15666,6 @@ packages: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} dependencies: dequal: 2.0.3 - dev: true /diacritics@1.3.0: resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} @@ -15358,6 +15694,10 @@ packages: htmlparser2: 3.10.1 dev: true + /dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -15545,6 +15885,11 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dev: false + /dotenv@7.0.0: resolution: {integrity: sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==} engines: {node: '>=6'} @@ -15574,6 +15919,12 @@ packages: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} + /duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + dependencies: + underscore: 1.13.7 + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -15627,6 +15978,11 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} requiresBuild: true @@ -15700,6 +16056,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + /enumify@1.0.4: + resolution: {integrity: sha512-5mwWXaVzJaqyUdOW/PDH5QySRgmQ8VvujmxmvXoXj9w0n+6omhVuyD56eI37FMqy/LxueJzsQ4DrHVQzuT/TXg==} + dev: false + /env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -17334,6 +17694,45 @@ packages: transitivePeerDependencies: - supports-color + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -17533,6 +17932,12 @@ packages: dependencies: reusify: 1.0.4 + /fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + dependencies: + format: 0.2.2 + dev: false + /fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: @@ -17768,6 +18173,21 @@ packages: transitivePeerDependencies: - supports-color + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -18030,6 +18450,10 @@ packages: typescript: 5.6.3 webpack: 5.91.0 + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -18042,6 +18466,19 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + dev: false + + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -18155,6 +18592,7 @@ packages: /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} + requiresBuild: true dependencies: minipass: 3.3.6 dev: false @@ -18164,6 +18602,7 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + requiresBuild: true /fs@0.0.1-security: resolution: {integrity: sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==} @@ -19385,6 +19824,7 @@ packages: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} deprecated: This package is no longer supported. + requiresBuild: true dependencies: aproba: 2.0.0 color-support: 1.1.3 @@ -19641,6 +20081,8 @@ packages: /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + requiresBuild: true dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -19788,6 +20230,18 @@ packages: unicorn-magic: 0.1.0 dev: true + /gm@1.25.0: + resolution: {integrity: sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==} + engines: {node: '>=14'} + dependencies: + array-parallel: 0.1.3 + array-series: 0.1.5 + cross-spawn: 4.0.2 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + dev: false + /gonzales-pe@4.3.0: resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} engines: {node: '>=0.6.0'} @@ -20083,6 +20537,7 @@ packages: /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + requiresBuild: true dev: false /has-value@0.3.1: @@ -20308,6 +20763,22 @@ packages: zwitch: 2.0.4 dev: false + /hast-util-to-html@9.0.4: + resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==} + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + dev: false + /hast-util-to-mdast@7.1.3: resolution: {integrity: sha512-3vER9p8B8mCs5b2qzoBiWlC9VnTkFmr8Ufb1eKdcvhVY+nipt52YfMRshk5r9gOE1IZ9/xtlSxebGCv1ig9uKA==} dependencies: @@ -20364,6 +20835,12 @@ packages: resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} dev: false + /hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + dependencies: + '@types/hast': 3.0.4 + dev: false + /hastscript@5.1.2: resolution: {integrity: sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==} dependencies: @@ -20449,6 +20926,13 @@ packages: resolution: {integrity: sha512-ycJQMRaRPBcfnoT1gS5I1XCvbbw9KO94Y0vkwksuOjcJMqNZtb03MF2tCItLI2mQbkZWSSeFinoRDPmjzv4tKg==} dev: true + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: false + /html-entities@2.5.2: resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} @@ -20456,6 +20940,10 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + dev: false + /html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} @@ -20469,6 +20957,10 @@ packages: resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} dev: false + /html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + dev: false + /htmlparser2@3.10.1: resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} dependencies: @@ -20495,7 +20987,15 @@ packages: domhandler: 5.0.3 domutils: 3.1.0 entities: 4.5.0 - dev: true + + /htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -20542,7 +21042,6 @@ packages: debug: 4.3.7 transitivePeerDependencies: - supports-color - dev: true /http-proxy-middleware@2.0.6(debug@4.3.4): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} @@ -20619,6 +21118,7 @@ packages: /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + requiresBuild: true dependencies: agent-base: 6.0.2 debug: 4.3.7 @@ -20646,6 +21146,16 @@ packages: - supports-color dev: true + /https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -20665,7 +21175,6 @@ packages: dependencies: ms: 2.1.3 dev: false - optional: true /husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} @@ -20740,6 +21249,13 @@ packages: dependencies: queue: 6.0.2 + /image-type@5.2.0: + resolution: {integrity: sha512-f0+6qHeGfyEh1HhFGPUWZb+Dqqm6raKeeAR6Opt01wBBIQL32/1wpZkPQm8gcliB/Ws6oiX2ofFYXB57+CV0iQ==} + engines: {node: '>=14.16'} + dependencies: + file-type: 18.7.0 + dev: false + /imagetools-core@6.0.4: resolution: {integrity: sha512-N1qs5qn7u9nR3kboISkYuvJm8MohiphCfBa+wx1UOropVaFis9/mh6wuDPLHJNhl6/64C7q2Pch5NASVKAaSrg==} engines: {node: '>=12.0.0'} @@ -20818,6 +21334,7 @@ packages: /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + requiresBuild: true dependencies: once: 1.4.0 wrappy: 1.0.2 @@ -20914,6 +21431,11 @@ packages: kind-of: 6.0.3 dev: false + /install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + dev: false + /internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -21023,6 +21545,10 @@ packages: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: false + /is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + dev: false + /is-alphanumeric@1.0.0: resolution: {integrity: sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==} engines: {node: '>=0.10.0'} @@ -21035,6 +21561,13 @@ packages: is-decimal: 1.0.4 dev: false + /is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + dev: false + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -21148,6 +21681,10 @@ packages: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} dev: false + /is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + dev: false + /is-descriptor@0.1.7: resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} engines: {node: '>= 0.4'} @@ -21206,6 +21743,7 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + requiresBuild: true /is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} @@ -21239,6 +21777,10 @@ packages: resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} dev: false + /is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + dev: false + /is-hotkey@0.1.8: resolution: {integrity: sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==} dev: false @@ -21370,6 +21912,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -22313,6 +22859,42 @@ packages: engines: {node: '>=12.0.0'} dev: true + /jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.1.2 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -22429,6 +23011,15 @@ packages: object.assign: 4.1.5 object.values: 1.2.0 + /jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + /junit-report-builder@3.2.1: resolution: {integrity: sha512-IMCp5XyDQ4YESDE4Za7im3buM0/7cMnRfe17k2X8B05FnUl9vqnaliX6cgOEmPIeWKfJrEe/gANRq/XgqttCqQ==} engines: {node: '>=8'} @@ -22463,6 +23054,13 @@ packages: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} dev: false + /katex@0.16.20: + resolution: {integrity: sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + /kebab-hash@0.1.2: resolution: {integrity: sha512-BTZpq3xgISmQmAVzkISy4eUutsUA7s4IEFlCwOBJjvSFOwyR7I+fza+tBc/rzYWK/NrmFHjfU1IhO3lu29Ib/w==} dependencies: @@ -22580,6 +23178,12 @@ packages: dependencies: immediate: 3.0.6 + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + dependencies: + immediate: 3.0.6 + dev: false + /light-my-request@5.12.0: resolution: {integrity: sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==} dependencies: @@ -22606,9 +23210,25 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + /linkedom@0.18.6: + resolution: {integrity: sha512-6G8euAJ84s7MTXTli5JIOO5tzEpyoUBw2/zcqAunSurbCtC83YcgrK+VTcO8HZ/rdR3eaaZM573FP9rNo1uXIA==} + dependencies: + css-select: 5.1.0 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 9.1.0 + uhyphen: 0.2.0 + dev: false + /linkfs@2.1.0: resolution: {integrity: sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew==} + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.1.0 + dev: false + /listhen@1.7.2: resolution: {integrity: sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g==} hasBin: true @@ -22971,6 +23591,14 @@ packages: dependencies: js-tokens: 4.0.0 + /lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.7 + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -23007,6 +23635,11 @@ packages: /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + /lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + dev: false + /lru-cache@4.0.0: resolution: {integrity: sha512-WKhDkjlLwzE8jAQdQlsxLUQTPXLCKX/4cJk6s5AlRtJkDBk0IKH5O51bVDH61K9N4bhbbyvLM6EiOuE8ovApPA==} dependencies: @@ -23080,6 +23713,7 @@ packages: /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} + requiresBuild: true dependencies: semver: 6.3.1 @@ -23126,6 +23760,23 @@ packages: tmpl: 1.0.5 dev: true + /mammoth@1.9.0: + resolution: {integrity: sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + '@xmldom/xmldom': 0.8.10 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.2 + path-is-absolute: 1.0.1 + underscore: 1.13.7 + xmlbuilder: 10.1.1 + dev: false + /map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} @@ -23171,6 +23822,18 @@ packages: resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==} dev: false + /markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + dev: false + /markdown-table@1.1.3: resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} dev: false @@ -23194,6 +23857,23 @@ packages: react: 18.2.0 dev: true + /markdownlint@0.37.3: + resolution: {integrity: sha512-eoQqH0291YCCjd+Pe1PUQ9AmWthlVmS0XWgcionkZ8q34ceZyRI+pYvsWksXJJL8OBkWCPwp1h/pnXxrPFC4oA==} + engines: {node: '>=18'} + dependencies: + markdown-it: 14.1.0 + micromark: 4.0.1 + micromark-core-commonmark: 2.0.2 + micromark-extension-directive: 3.0.2 + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-math: 3.1.0 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /material-colors@1.2.6: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false @@ -23261,6 +23941,15 @@ packages: unist-util-visit-parents: 6.0.1 dev: true + /mdast-util-footnote@0.1.7: + resolution: {integrity: sha512-QxNdO8qSxqbO2e3m09KwDKfWiLgqyCurdWTQ198NpbZ2hxntdc+VKS4fDJCmNWbAroUdYnSthu+XbZ8ovh8C3w==} + dependencies: + mdast-util-to-markdown: 0.6.5 + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + dev: false + /mdast-util-from-markdown@0.8.5: resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} dependencies: @@ -23311,6 +24000,31 @@ packages: - supports-color dev: true + /mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.2 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-frontmatter@0.2.0: + resolution: {integrity: sha512-FHKL4w4S5fdt1KjJCwB0178WJ0evnyyQr5kXTM3wrOVpytD0hrkvd+AOOjU9Td8onOejCkmZ+HQRT3CZ3coHHQ==} + dependencies: + micromark-extension-frontmatter: 0.2.2 + dev: false + /mdast-util-gfm-autolink-literal@0.1.3: resolution: {integrity: sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==} dependencies: @@ -23475,6 +24189,20 @@ packages: unist-util-visit: 4.1.2 dev: false + /mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + dev: false + /mdast-util-to-hast@4.0.0: resolution: {integrity: sha512-yOTZSxR1aPvWRUxVeLaLZ1sCYrK87x2Wusp1bDM/Ao2jETBhYUKITI3nHvgy+HkZW54HuCAhHnS0mTcbECD5Ig==} dependencies: @@ -23546,7 +24274,6 @@ packages: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} dependencies: '@types/mdast': 4.0.4 - dev: true /mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -23561,6 +24288,10 @@ packages: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} dev: false + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + dev: false + /meant@1.0.3: resolution: {integrity: sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw==} @@ -23667,6 +24398,10 @@ packages: /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + /merge-options@3.0.4: resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} engines: {node: '>=10'} @@ -23750,7 +24485,52 @@ packages: micromark-util-subtokenize: 2.0.1 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true + + /micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.1 + + /micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.1 + parse-entities: 4.0.2 + dev: false + + /micromark-extension-footnote@0.3.2: + resolution: {integrity: sha512-gr/BeIxbIWQoUm02cIfK7mdMZ/fbroRpLsck4kvFtjbzP4yi+OPVbnukTc/zy0i7spC2xYE/dbX1Sur8BEDJsQ==} + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + dev: false + + /micromark-extension-frontmatter@0.2.2: + resolution: {integrity: sha512-q6nPLFCMTLtfsctAuS0Xh4vaolxSFUWUWR6PZSrXXiRy+SANGllpcqdXFv2z07l0Xz/6Hl40hK0ffNCJPH2n1A==} + dependencies: + fault: 1.0.4 + dev: false /micromark-extension-gfm-autolink-literal@0.5.7: resolution: {integrity: sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==} @@ -23766,21 +24546,19 @@ packages: micromark-util-character: 2.1.0 micromark-util-sanitize-uri: 2.0.0 micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-types: 2.0.1 /micromark-extension-gfm-footnote@2.1.0: resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} dependencies: devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 + micromark-core-commonmark: 2.0.2 micromark-factory-space: 2.0.0 micromark-util-character: 2.1.0 micromark-util-normalize-identifier: 2.0.0 micromark-util-sanitize-uri: 2.0.0 micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-types: 2.0.1 /micromark-extension-gfm-strikethrough@0.6.5: resolution: {integrity: sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==} @@ -23816,8 +24594,7 @@ packages: micromark-factory-space: 2.0.0 micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: true + micromark-util-types: 2.0.1 /micromark-extension-gfm-tagfilter@0.3.0: resolution: {integrity: sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==} @@ -23873,6 +24650,18 @@ packages: micromark-util-types: 2.0.0 dev: true + /micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + dependencies: + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.20 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.1 + dev: false + /micromark-factory-destination@1.1.0: resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} dependencies: @@ -23887,7 +24676,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-factory-label@1.1.0: resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} @@ -23905,7 +24693,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-factory-space@1.1.0: resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} @@ -23919,7 +24706,6 @@ packages: dependencies: micromark-util-character: 2.1.0 micromark-util-types: 2.0.0 - dev: true /micromark-factory-title@1.1.0: resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} @@ -23937,7 +24723,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-factory-whitespace@1.1.0: resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} @@ -23955,7 +24740,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-util-character@1.2.0: resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} @@ -23969,7 +24753,6 @@ packages: dependencies: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-util-chunked@1.1.0: resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} @@ -23981,7 +24764,6 @@ packages: resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} dependencies: micromark-util-symbol: 2.0.0 - dev: true /micromark-util-classify-character@1.1.0: resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} @@ -23997,7 +24779,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-util-combine-extensions@1.1.0: resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} @@ -24011,7 +24792,6 @@ packages: dependencies: micromark-util-chunked: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-util-decode-numeric-character-reference@1.1.0: resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} @@ -24023,7 +24803,6 @@ packages: resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} dependencies: micromark-util-symbol: 2.0.0 - dev: true /micromark-util-decode-string@1.1.0: resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} @@ -24041,7 +24820,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-decode-numeric-character-reference: 2.0.1 micromark-util-symbol: 2.0.0 - dev: true /micromark-util-encode@1.1.0: resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} @@ -24049,7 +24827,6 @@ packages: /micromark-util-encode@2.0.0: resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - dev: true /micromark-util-html-tag-name@1.2.0: resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} @@ -24057,7 +24834,6 @@ packages: /micromark-util-html-tag-name@2.0.0: resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} - dev: true /micromark-util-normalize-identifier@1.1.0: resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} @@ -24069,7 +24845,6 @@ packages: resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} dependencies: micromark-util-symbol: 2.0.0 - dev: true /micromark-util-resolve-all@1.1.0: resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} @@ -24081,7 +24856,6 @@ packages: resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} dependencies: micromark-util-types: 2.0.0 - dev: true /micromark-util-sanitize-uri@1.2.0: resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} @@ -24097,7 +24871,6 @@ packages: micromark-util-character: 2.1.0 micromark-util-encode: 2.0.0 micromark-util-symbol: 2.0.0 - dev: true /micromark-util-subtokenize@1.1.0: resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} @@ -24115,7 +24888,6 @@ packages: micromark-util-chunked: 2.0.0 micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - dev: true /micromark-util-symbol@1.1.0: resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} @@ -24123,7 +24895,6 @@ packages: /micromark-util-symbol@2.0.0: resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} - dev: true /micromark-util-types@1.1.0: resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} @@ -24131,12 +24902,14 @@ packages: /micromark-util-types@2.0.0: resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} - dev: true + + /micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} /micromark@2.11.4: resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} dependencies: - debug: 4.3.7 + debug: 4.4.0 parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -24188,7 +24961,30 @@ packages: micromark-util-types: 2.0.0 transitivePeerDependencies: - supports-color - dev: true + + /micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false /micromatch@3.1.10: resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} @@ -24284,6 +25080,13 @@ packages: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + /mimic-response@2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -24320,6 +25123,7 @@ packages: /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + requiresBuild: true dependencies: brace-expansion: 1.1.11 @@ -24412,6 +25216,7 @@ packages: /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} + requiresBuild: true dependencies: yallist: 4.0.0 dev: false @@ -24419,6 +25224,7 @@ packages: /minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + requiresBuild: true dev: false /minipass@7.0.4: @@ -24433,6 +25239,7 @@ packages: /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + requiresBuild: true dependencies: minipass: 3.3.6 yallist: 4.0.0 @@ -24463,6 +25270,7 @@ packages: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true + requiresBuild: true /mlly@1.6.1: resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} @@ -24669,6 +25477,11 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + /neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + dev: false + /nested-error-stacks@2.1.1: resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} dev: false @@ -24902,6 +25715,10 @@ packages: engines: {node: '>=10.5.0'} dev: false + /node-ensure@0.0.0: + resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==} + dev: false + /node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} dev: false @@ -25049,6 +25866,7 @@ packages: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} hasBin: true + requiresBuild: true dependencies: abbrev: 1.1.1 dev: false @@ -25141,6 +25959,7 @@ packages: /npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. + requiresBuild: true dependencies: are-we-there-yet: 2.0.0 console-control-strings: 1.1.0 @@ -25179,6 +25998,10 @@ packages: /nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + /nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + dev: false + /nyc@15.1.0: resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==} engines: {node: '>=8.9'} @@ -25218,6 +26041,7 @@ packages: /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + requiresBuild: true /object-copy@0.1.0: resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} @@ -25371,6 +26195,7 @@ packages: /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + requiresBuild: true dependencies: wrappy: 1.0.2 @@ -25413,6 +26238,26 @@ packages: is-docker: 2.2.1 is-wsl: 2.2.0 + /openai@4.78.1: + resolution: {integrity: sha512-drt0lHZBd2lMyORckOXFPQTmnGLWSLt8VK0W9BhOKWpMFBEoHMoz5gxMPmVq5icp+sOrsbMnsmZTVHUlKvD1Ow==} + hasBin: true + peerDependencies: + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + dependencies: + '@types/node': 18.19.70 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /opentracing@0.14.7: resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} engines: {node: '>=0.10'} @@ -25422,6 +26267,10 @@ packages: dependencies: '@wry/context': 0.4.4 + /option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + dev: false + /optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -25716,6 +26565,10 @@ packages: registry-url: 6.0.1 semver: 7.6.3 + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + /pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} dev: false @@ -25762,6 +26615,18 @@ packages: is-hexadecimal: 1.0.4 dev: false + /parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + dependencies: + '@types/unist': 2.0.10 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + dev: false + /parse-filepath@1.0.2: resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} engines: {node: '>=0.8'} @@ -25818,6 +26683,10 @@ packages: dependencies: protocols: 2.0.1 + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: false + /parse-url@8.1.0: resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} dependencies: @@ -25842,7 +26711,6 @@ packages: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: entities: 4.5.0 - dev: true /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -25932,6 +26800,10 @@ packages: lru-cache: 10.4.3 minipass: 7.1.2 + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -25977,6 +26849,15 @@ packages: resolve-protobuf-schema: 2.1.0 dev: false + /pdf2pic@3.1.3: + resolution: {integrity: sha512-KbW4Qb7iHw2fBRWtA9FTc4pZg9cokiFIzc6cE7dzelTrhXWolfQuG1fYVC0E2BRmK/w7xfBjQ+OEsPZPO3QEew==} + engines: {node: '>=14'} + dependencies: + gm: 1.25.0 + transitivePeerDependencies: + - supports-color + dev: false + /peek-readable@4.1.0: resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} engines: {node: '>=8'} @@ -27243,6 +28124,11 @@ packages: end-of-stream: 1.4.4 once: 1.4.0 + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: false + /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -27292,6 +28178,13 @@ packages: side-channel: 1.0.6 dev: false + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /query-string@6.14.1: resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==} engines: {node: '>=6'} @@ -28527,6 +29420,22 @@ packages: es6-error: 4.1.1 dev: true + /remark-footnotes@3.0.0: + resolution: {integrity: sha512-ZssAvH9FjGYlJ/PBVKdSmfyPc3Cz4rTWgZLI4iE/SX8Nt5l3o3oEjv3wwG5VD7xOjktzdwp5coac+kJV9l4jgg==} + dependencies: + mdast-util-footnote: 0.1.7 + micromark-extension-footnote: 0.3.2 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-frontmatter@3.0.0: + resolution: {integrity: sha512-mSuDd3svCHs+2PyO29h7iijIZx4plX0fheacJcAoYAASfgzgVIcXGYSq9GFyYocFLftQs8IOmmkgtOovs6d4oA==} + dependencies: + mdast-util-frontmatter: 0.2.0 + micromark-extension-frontmatter: 0.2.2 + dev: false + /remark-gfm@1.0.0: resolution: {integrity: sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==} dependencies: @@ -28920,6 +29829,14 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.14.1 fsevents: 2.3.3 + /rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + dev: false + + /rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + dev: false + /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -29006,10 +29923,28 @@ packages: dependencies: truncate-utf8-bytes: 1.0.2 + /sanitize-html@2.13.1: + resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.49 + dev: false + /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /scheduler@0.20.2: resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} dependencies: @@ -29111,6 +30046,7 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + requiresBuild: true /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} @@ -29131,6 +30067,7 @@ packages: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true + requiresBuild: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -29152,6 +30089,27 @@ packages: transitivePeerDependencies: - supports-color + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: @@ -29252,6 +30210,18 @@ packages: transitivePeerDependencies: - supports-color + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /serve@14.2.1: resolution: {integrity: sha512-48er5fzHh7GCShLnNyPBRPEjs2I6QBozeGr02gaacROiyS/8ARADlj595j39iZXAqBbJHH/ivJJyPRWY9sQWZA==} engines: {node: '>= 14'} @@ -29282,6 +30252,7 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + requiresBuild: true /set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} @@ -29409,6 +30380,7 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + requiresBuild: true /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -29419,8 +30391,19 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + requiresBuild: true dev: false + /simple-get@3.1.1: + resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + requiresBuild: true + dependencies: + decompress-response: 4.2.1 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + optional: true + /simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} dependencies: @@ -30118,6 +31101,7 @@ packages: /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + requiresBuild: true dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 @@ -30238,6 +31222,7 @@ packages: /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + requiresBuild: true dependencies: ansi-regex: 5.0.1 @@ -30472,6 +31457,10 @@ packages: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -30610,6 +31599,7 @@ packages: /tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + requiresBuild: true dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 @@ -30921,6 +31911,17 @@ packages: dependencies: tslib: 2.6.2 + /tldts-core@6.1.71: + resolution: {integrity: sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==} + dev: false + + /tldts@6.1.71: + resolution: {integrity: sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==} + hasBin: true + dependencies: + tldts-core: 6.1.71 + dev: false + /tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} dependencies: @@ -31020,6 +32021,13 @@ packages: engines: {node: '>=6'} dev: true + /tough-cookie@5.1.0: + resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} + engines: {node: '>=16'} + dependencies: + tldts: 6.1.71 + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -31029,6 +32037,13 @@ packages: punycode: 2.3.1 dev: true + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + dev: false + /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -31327,6 +32342,12 @@ packages: turbo-windows-arm64: 2.0.6 dev: true + /turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + dependencies: + '@mixmark-io/domino': 2.2.0 + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -31476,6 +32497,10 @@ packages: /ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + /uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + dev: false + /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -31487,6 +32512,10 @@ packages: dev: true optional: true + /uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + dev: false + /uid-safe@2.1.5: resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} engines: {node: '>= 0.8'} @@ -31522,12 +32551,15 @@ packages: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} dev: false + /underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} /undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - dev: true /unenv@1.9.0: resolution: {integrity: sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==} @@ -31692,7 +32724,6 @@ packages: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} dependencies: '@types/unist': 3.0.2 - dev: true /unist-util-position@3.1.0: resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} @@ -31704,6 +32735,12 @@ packages: '@types/unist': 2.0.10 dev: false + /unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-remove-position@1.1.4: resolution: {integrity: sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==} dependencies: @@ -31726,7 +32763,6 @@ packages: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} dependencies: '@types/unist': 3.0.2 - dev: true /unist-util-visit-parents@2.1.2: resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} @@ -31753,7 +32789,6 @@ packages: dependencies: '@types/unist': 3.0.2 unist-util-is: 6.0.0 - dev: true /unist-util-visit@1.4.1: resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} @@ -31783,7 +32818,6 @@ packages: '@types/unist': 3.0.2 unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - dev: true /universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} @@ -31809,6 +32843,15 @@ packages: dependencies: normalize-path: 2.1.1 + /unpdf@0.12.1: + resolution: {integrity: sha512-ktP8+TTLDBrlu/j8rQVNbHoMMpFXzkVAkb1rt/JdshFC3jOHdZjuGCNl/voPL0kraUrUOH7ZC88kVxMvlvDBzA==} + optionalDependencies: + canvas: 2.11.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -32185,7 +33228,6 @@ packages: dependencies: '@types/unist': 3.0.2 unist-util-stringify-position: 4.0.0 - dev: true /vfile@4.2.1: resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} @@ -32210,7 +33252,6 @@ packages: dependencies: '@types/unist': 3.0.2 vfile-message: 4.0.2 - dev: true /vite-imagetools@6.2.9: resolution: {integrity: sha512-C4ZYhgj2vAj43/TpZ06XlDNP0p/7LIeYbgUYr+xG44nM++4HGX6YZBKAYpiBNgiCFUTJ6eXkRppWBrfPMevgmg==} @@ -32657,6 +33698,13 @@ packages: - terser dev: true + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: false + /wait-on@7.2.0: resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} engines: {node: '>=12.0.0'} @@ -32753,6 +33801,11 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + /web-worker@1.3.0: resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} dev: false @@ -33029,6 +34082,13 @@ packages: dependencies: iconv-lite: 0.6.3 + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: false + /whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} dev: false @@ -33037,6 +34097,19 @@ packages: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: false + + /whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -33122,6 +34195,7 @@ packages: /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + requiresBuild: true dependencies: string-width: 4.2.3 dev: false @@ -33221,6 +34295,7 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + requiresBuild: true /write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} @@ -33295,6 +34370,19 @@ packages: utf-8-validate: optional: true + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xdg-basedir@4.0.0: resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} engines: {node: '>=8'} @@ -33304,6 +34392,11 @@ packages: engines: {node: '>=12'} dev: false + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: false + /xml-utils@1.8.0: resolution: {integrity: sha512-1TY5yLw8DApowZAUsWCniNr8HH6Ebt6O7UQvmIwziGKwUNsQx6e+4NkfOvCfnqmYIcPjCeoI6dh1JenPJ9a1hQ==} dev: false @@ -33312,11 +34405,20 @@ packages: resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} dev: true + /xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + dev: false + /xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} dev: true + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xmlhttprequest-ssl@2.0.0: resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} engines: {node: '>=0.4.0'} @@ -33355,6 +34457,7 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + requiresBuild: true /yaml-ast-parser@0.0.43: resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} @@ -33556,3 +34659,15 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + + github.com/iamh2o/pdf-parse/d7a41d5aaed1503bee2d7ea50bf89588d3b2d2cf: + resolution: {tarball: https://codeload.github.com/iamh2o/pdf-parse/tar.gz/d7a41d5aaed1503bee2d7ea50bf89588d3b2d2cf} + name: pdf-parse + version: 1.1.3 + engines: {node: '>=6.8.1'} + dependencies: + debug: 3.2.7 + node-ensure: 0.0.0 + transitivePeerDependencies: + - supports-color + dev: false