This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 4f8b5aaa2 Add documentation debug logging.
new 1c79eba25 Merge branch 'master' of
https://gitbox.apache.org/repos/asf/juneau.git
4f8b5aaa2 is described below
commit 4f8b5aaa26fb616600bd163526f1bdd3f86562f2
Author: James Bognar <[email protected]>
AuthorDate: Tue Sep 23 09:30:06 2025 -0400
Add documentation debug logging.
---
juneau-docs-poc/docusaurus.config.ts | 43 +-
juneau-docs-poc/html-to-md-converter.js | 991 +++++++++++++++++++++
.../src/plugins/remark-version-replacer.js | 74 +-
3 files changed, 1034 insertions(+), 74 deletions(-)
diff --git a/juneau-docs-poc/docusaurus.config.ts
b/juneau-docs-poc/docusaurus.config.ts
index 7605c79a1..b136947e9 100644
--- a/juneau-docs-poc/docusaurus.config.ts
+++ b/juneau-docs-poc/docusaurus.config.ts
@@ -18,6 +18,8 @@ import remarkJuneauLinks from
'./src/plugins/remark-juneau-links';
const remarkVersionReplacer = require('./src/plugins/remark-version-replacer');
// This runs in Node.js - Don't use client-side code here (browser APIs,
JSX...)
+console.log('🚀 Docusaurus config loading...');
+console.log('📦 remarkVersionReplacer loaded:', typeof remarkVersionReplacer);
const config: Config = {
title: 'Apache Juneau',
@@ -67,25 +69,28 @@ const config: Config = {
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/apache/juneau/tree/main/juneau-docs-poc/juneau-documentation/',
- remarkPlugins: [
- [remarkJuneauLinks, {
- packageAbbreviations: {
- 'oaj': 'org.apache.juneau',
- 'oajr': 'org.apache.juneau.rest',
- 'oajrc': 'org.apache.juneau.rest.client',
- 'oajrs': 'org.apache.juneau.rest.server',
- 'oajrss': 'org.apache.juneau.rest.server.springboot',
- 'oajrm': 'org.apache.juneau.rest.mock',
- 'oajmc': 'org.apache.juneau.microservice.core',
- 'oajmj': 'org.apache.juneau.microservice.jetty',
- },
- javadocBaseUrl: '../apidocs'
- }],
- [remarkVersionReplacer, {
- version: '9.0.1',
- apiDocsUrl: '../apidocs'
- }]
- ]
+ remarkPlugins: (() => {
+ console.log('🔧 Setting up remark plugins...');
+ return [
+ [remarkJuneauLinks, {
+ packageAbbreviations: {
+ 'oaj': 'org.apache.juneau',
+ 'oajr': 'org.apache.juneau.rest',
+ 'oajrc': 'org.apache.juneau.rest.client',
+ 'oajrs': 'org.apache.juneau.rest.server',
+ 'oajrss': 'org.apache.juneau.rest.server.springboot',
+ 'oajrm': 'org.apache.juneau.rest.mock',
+ 'oajmc': 'org.apache.juneau.microservice.core',
+ 'oajmj': 'org.apache.juneau.microservice.jetty',
+ },
+ javadocBaseUrl: '../apidocs'
+ }],
+ [remarkVersionReplacer, {
+ version: '9.0.1',
+ apiDocsUrl: '../apidocs'
+ }]
+ ];
+ })()
},
blog: {
showReadingTime: true,
diff --git a/juneau-docs-poc/html-to-md-converter.js
b/juneau-docs-poc/html-to-md-converter.js
new file mode 100644
index 000000000..9f51f74c3
--- /dev/null
+++ b/juneau-docs-poc/html-to-md-converter.js
@@ -0,0 +1,991 @@
+#!/usr/bin/env node
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+
+const fs = require('fs');
+const path = require('path');
+
+// Package abbreviations for {@link} processing
+const packageAbbreviations = {
+ 'oaj': 'org.apache.juneau',
+ 'oajr': 'org.apache.juneau.rest',
+ 'oajrc': 'org.apache.juneau.rest.client',
+ 'oajrs': 'org.apache.juneau.rest.server',
+ 'oajrss': 'org.apache.juneau.rest.server.springboot',
+ 'oajrm': 'org.apache.juneau.rest.mock',
+ 'oajmc': 'org.apache.juneau.microservice.core',
+ 'oajmj': 'org.apache.juneau.microservice.jetty',
+};
+
+function convertHtmlToMarkdown(htmlContent, sourceFile) {
+ let markdown = htmlContent;
+
+ // Remove HTML DOCTYPE and html/head/body structure
+ markdown = markdown.replace(/<!DOCTYPE[^>]*>/gi, '');
+ markdown = markdown.replace(/<html[^>]*>/gi, '');
+ markdown = markdown.replace(/<\/html>/gi, '');
+ markdown = markdown.replace(/<head>[\s\S]*?<\/head>/gi, '');
+ markdown = markdown.replace(/<body[^>]*>/gi, '');
+ markdown = markdown.replace(/<\/body>/gi, '');
+
+ // Remove HTML comments
+ markdown = markdown.replace(/<!--[\s\S]*?-->/gi, '');
+
+ // Extract title from HTML or use filename
+ let title = 'Documentation';
+ const titleMatch = markdown.match(/<title>(.*?)<\/title>/i);
+ if (titleMatch) {
+ title = titleMatch[1].replace(/^\d+\.\s*/, '');
+ } else {
+ // Extract title from {title:'...'} patterns
+ const juneauTitleMatch =
markdown.match(/\{title:\s*['"]([^'"]*)['"]/i);
+ if (juneauTitleMatch) {
+ title = juneauTitleMatch[1];
+ }
+ }
+
+ // Remove title and meta tags
+ markdown = markdown.replace(/<title>.*?<\/title>/gi, '');
+ markdown = markdown.replace(/<meta[^>]*>/gi, '');
+ markdown = markdown.replace(/<link[^>]*>/gi, '');
+
+ // Remove Juneau-specific metadata
+ markdown = markdown.replace(/\{title:\s*[^}]*\}/gi, '');
+ markdown = markdown.replace(/\{created:\s*[^}]*\}/gi, '');
+ markdown = markdown.replace(/\{updated:\s*[^}]*\}/gi, '');
+
+ // Remove divs with class="topic" but keep content
+ markdown = markdown.replace(/<div\s+class=['"]topic['"][^>]*>/gi, '');
+ markdown = markdown.replace(/<\/div>/gi, '');
+
+ // Convert HTML entities FIRST, before other processing (except in BXML
blocks)
+ // BXML blocks need special handling to preserve XML structure
+ markdown =
markdown.replace(/(<p\s+class=['"][^'"]*\bbxml(?:\s+[^'"]*)['"][^>]*>[\s\S]*?<\/p>)|</gi,
(match, bxmlBlock) => {
+ return bxmlBlock || '<';
+ });
+ markdown =
markdown.replace(/(<p\s+class=['"][^'"]*\bbxml(?:\s+[^'"]*)['"][^>]*>[\s\S]*?<\/p>)|>/gi,
(match, bxmlBlock) => {
+ return bxmlBlock || '>';
+ });
+ markdown = markdown.replace(/&/g, '&');
+ markdown = markdown.replace(/"/g, '"');
+ markdown = markdown.replace(/'/g, "'");
+ markdown = markdown.replace(/ /g, ' ');
+
+ // Convert all code block types to proper Markdown with language hints
+ // Updated regex to handle width classes like 'bxml w500', 'bcode w800',
etc.
+ markdown =
markdown.replace(/<p\s+class=['"][^'"]*b(java|bjavacode|json|xml|bxml|ini|console|uon|urlenc|code)(?:\s+[^'"]*)?['"][^>]*>((?:(?!<\/p>)[\s\S])*?)<\/p>/gi,
(match, type, codeContent) => {
+
+ // SIMPLE & RELIABLE DOCGENERATOR CONVERTER
+ // Focus on getting indentation and empty lines right
+
+ let lines = codeContent.split('\n');
+ let processedLines = [];
+
+ for (let line of lines) {
+ // Match DocGenerator pipe pattern: "|" + content
+ const pipeMatch = line.match(/^\s*\|(.*)$/);
+ if (!pipeMatch) {
+ // Skip non-pipe lines (but don't skip empty lines that might
have pipes)
+ if (line.trim() !== '') {
+ continue;
+ }
+ // For completely empty lines, skip them
+ continue;
+ }
+
+ const afterPipe = pipeMatch[1];
+
+ // Handle empty lines (just whitespace after pipe)
+ if (afterPipe.trim() === '') {
+ processedLines.push('');
+ continue;
+ }
+
+ // Parse indentation and content
+ const contentMatch = afterPipe.match(/^(\s*)(.*)$/);
+ if (!contentMatch) {
+ processedLines.push(afterPipe.trim());
+ continue;
+ }
+
+ const [, indentChars, actualContent] = contentMatch;
+
+ // Calculate indentation level
+ const tabCount = (indentChars.match(/\t/g) || []).length;
+ const spaceCount = indentChars.length;
+ let indentLevel = 0;
+
+
+ if (tabCount > 0) {
+ // Tab-based: |\t = level 0, |\t\t = level 1
+ indentLevel = Math.max(0, tabCount - 1);
+ } else {
+ // Space-based: |<spaces>
+ if (spaceCount >= 15) {
+ indentLevel = Math.floor((spaceCount - 7) / 8);
+ } else if (spaceCount >= 7) {
+ indentLevel = 0;
+ } else {
+ indentLevel = 0;
+ }
+ }
+
+
+ // Apply indentation
+ const finalIndentation = ' '.repeat(indentLevel);
+ const cleanContent = actualContent.trim();
+ processedLines.push(finalIndentation + cleanContent);
+ }
+
+ // Join lines and preserve structure
+ let code = processedLines.join('\n');
+
+
+ // Process Juneau tags based on code type
+ if (type === 'java' || type === 'bjavacode') {
+ // Java-specific tags
+ code = code.replace(/<jk>(.*?)<\/jk>/gi, '$1'); // keywords
+ code = code.replace(/<js>(.*?)<\/js>/gi, '$1'); // strings
+ code = code.replace(/<jc>(.*?)<\/jc>/gi, '$1'); // comments
+ code = code.replace(/<ja>(.*?)<\/ja>/gi, '$1'); // annotations
+ code = code.replace(/<jf>(.*?)<\/jf>/gi, '$1'); // fields
+ code = code.replace(/<jsm>(.*?)<\/jsm>/gi, '$1'); // static methods
+ code = code.replace(/<jm>(.*?)<\/jm>/gi, '$1'); // methods
+ code = code.replace(/<jv>(.*?)<\/jv>/gi, '$1'); // variables
+ code = code.replace(/<jp>(.*?)<\/jp>/gi, '$1'); // parameters
+ } else if (type === 'json') {
+ // JSON-specific tags
+ code = code.replace(/<jok>(.*?)<\/jok>/gi, '$1'); // JSON object
keys
+ code = code.replace(/<jov>(.*?)<\/jov>/gi, '$1'); // JSON object
values
+ code = code.replace(/<joc>(.*?)<\/joc>/gi, '$1'); // JSON comments
+ } else if (type === 'xml') {
+ // XML-specific tags
+ code = code.replace(/<xt>(.*?)<\/xt>/gi, '$1'); // XML tags
+ } else if (type === 'ini') {
+ // INI/Config-specific tags
+ code = code.replace(/<cc>(.*?)<\/cc>/gi, '$1'); // comments
+ code = code.replace(/<cs>(.*?)<\/cs>/gi, '$1'); // sections
+ code = code.replace(/<ck>(.*?)<\/ck>/gi, '$1'); // keys
+ code = code.replace(/<cv>(.*?)<\/cv>/gi, '$1'); // values
+ } else if (type === 'uon' || type === 'urlenc') {
+ // UON/URL encoding may have string literals
+ code = code.replace(/<js>(.*?)<\/js>/gi, '$1'); // strings
+ }
+
+ // Remove any remaining HTML tags (except for BXML which needs special
handling)
+ if (type !== 'bxml') {
+ code = code.replace(/<[^>]*>/g, '');
+ }
+
+ // BXML-specific processing: Handle after other tag removal to
preserve XML structure
+ if (type === 'bxml') {
+ // First remove the Juneau-specific tags while preserving their
content
+ code = code.replace(/<xt>(.*?)<\/xt>/gi, '$1'); // XML tags
+ code = code.replace(/<xv>(.*?)<\/xv>/gi, '$1'); // XML values
+ code = code.replace(/<xa>(.*?)<\/xa>/gi, '$1'); // XML attributes
+ // Then decode HTML entities to restore XML structure
+ code = code.replace(/</g, '<');
+ code = code.replace(/>/g, '>');
+ code = code.replace(/&/g, '&');
+ code = code.replace(/"/g, '"');
+ code = code.replace(/'/g, "'");
+ // Remove any remaining HTML tags (but not the XML tags we just
created)
+ code = code.replace(/<(?!\/?[a-zA-Z][a-zA-Z0-9]*\b[^>]*>)[^>]*>/g,
'');
+ }
+
+ // For regular XML blocks (not bxml), also decode HTML entities
+ if (type === 'xml') {
+ code = code.replace(/</g, '<');
+ code = code.replace(/>/g, '>');
+ code = code.replace(/&/g, '&');
+ code = code.replace(/"/g, '"');
+ code = code.replace(/'/g, "'");
+ }
+
+ // Map code types to appropriate language hints for syntax highlighting
+ const languageMap = {
+ 'java': 'java',
+ 'bjavacode': 'java', // bbjavacode is also Java
+ 'json': 'json',
+ 'xml': 'xml',
+ 'bxml': 'xml', // BXML blocks are converted to XML syntax
highlighting
+ 'ini': 'ini',
+ 'console': 'bash',
+ 'uon': 'javascript', // Closest approximation
+ 'urlenc': 'text',
+ 'code': 'text'
+ };
+
+ const language = languageMap[type] || 'text';
+
+
+ return '\n```' + language + '\n' + code + '\n```\n';
+ });
+
+ // Convert headings (h1-h6) to markdown
+ markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1');
+ markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1');
+ markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1');
+ markdown = markdown.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1');
+ markdown = markdown.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1');
+ markdown = markdown.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1');
+
+ // Convert Java code blocks (look for multi-line code patterns)
+ // DISABLED: This was interfering with the main code block conversion above
+ // markdown = convertCodeBlocks(markdown);
+
+ // Fix malformed code blocks (multiple consecutive ```java blocks)
+ // DISABLED FOR DEBUGGING: markdown =
markdown.replace(/```\s*\n\s*```java\s*\n/g, '\n');
+
+ // Fix code blocks split across multiple p tags by merging adjacent ones
+ // DISABLED FOR DEBUGGING: markdown =
markdown.replace(/```\n\n```java\n/g, '\n');
+
+ // Convert inline code with custom Juneau tags
+ markdown = markdown.replace(/<c>(.*?)<\/c>/gi, (match, content) => {
+ // Process Juneau-specific tags within code
+ content = content.replace(/<jk>(.*?)<\/jk>/gi, '$1'); // keywords
+ content = content.replace(/<js>(.*?)<\/js>/gi, '$1'); // strings
+ content = content.replace(/<jc>(.*?)<\/jc>/gi, '$1'); // comments
+ content = content.replace(/<ja>(.*?)<\/ja>/gi, '$1'); // annotations
+ content = content.replace(/<jf>(.*?)<\/jf>/gi, '$1'); // fields
+ content = content.replace(/<jsm>(.*?)<\/jsm>/gi, '$1'); // static
methods
+ content = content.replace(/<jm>(.*?)<\/jm>/gi, '$1'); // methods
+ content = content.replace(/<jv>(.*?)<\/jv>/gi, '$1'); // variables
+ content = content.replace(/<jp>(.*?)<\/jp>/gi, '$1'); // parameters
+ return '`' + content + '`';
+ });
+
+ // Convert paragraphs
+ markdown = markdown.replace(/<p[^>]*>/gi, '\n\n');
+ markdown = markdown.replace(/<\/p>/gi, '\n\n');
+
+ // Convert line breaks
+ markdown = markdown.replace(/<br[^>]*>/gi, '\n');
+
+ // Convert bold/strong
+ markdown = markdown.replace(/<(b|strong)[^>]*>(.*?)<\/\1>/gi, '**$2**');
+
+ // Convert italic/emphasis
+ markdown = markdown.replace(/<(i|em)[^>]*>(.*?)<\/\1>/gi, '*$2*');
+
+ // Convert doclinks to TODO links first (before processing special
sections)
+ markdown =
markdown.replace(/<a[^>]*class=['"]doclink['"][^>]*href=['"]#jm\.([^'"]*)['"][^>]*>(.*?)<\/a>/gi,
'[TODO: $2](TODO.md)');
+
+ // Handle special note list items - convert to note blocks with icons
+ markdown =
markdown.replace(/<li[^>]*class=['"]note['"][^>]*>([\s\S]*?)<\/li>/gi,
'\n:::note\n$1\n:::\n');
+
+ // Handle "See Also" sections - convert to Docusaurus info admonition
blocks (BEFORE general ul processing)
+ markdown =
markdown.replace(/<ul[^>]*class=['"]seealso['"][^>]*>([\s\S]*?)<\/ul>/gi,
(match, content) => {
+ // Extract the link content
+ const linkMatch =
content.match(/<li[^>]*class=['"]link['"][^>]*>([\s\S]*?)<\/li>/gi);
+ if (linkMatch) {
+ let seeAlsoContent = '\n:::info See Also\n\n';
+ linkMatch.forEach(link => {
+ const linkContent =
link.replace(/<li[^>]*class=['"]link['"][^>]*>([\s\S]*?)<\/li>/gi, '$1');
+ seeAlsoContent += `🔗 ${linkContent}\n\n`;
+ });
+ seeAlsoContent += ':::\n';
+ return seeAlsoContent;
+ }
+ return '\n:::info See Also\n\n' + content + '\n\n:::\n';
+ });
+
+ // Convert simple lists (ul/ol)
+ markdown = markdown.replace(/<ul[^>]*>/gi, '\n');
+ markdown = markdown.replace(/<\/ul>/gi, '\n');
+ markdown = markdown.replace(/<ol[^>]*>/gi, '\n');
+ markdown = markdown.replace(/<\/ol>/gi, '\n');
+
+ // Handle proper <li>...</li> pairs first
+ markdown = markdown.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n');
+
+ // Handle standalone <li> tags without closing tags (common in Juneau docs)
+ markdown = markdown.replace(/<li[^>]*>([^<]*?)(?=\n\s*<li|$)/gi, '- $1\n');
+ markdown = markdown.replace(/<li[^>]*>([^<]*?)(?=\n)/gi, '- $1\n');
+
+ // Clean up stray HTML tags that might be left over
+ markdown = markdown.replace(/<\/?li[^>]*>/gi, ''); // Remove any remaining
<li> or </li> tags
+ markdown = markdown.replace(/<\/?[a-z]+[^>]*>/gi, ''); // Remove any
remaining HTML tags
+
+ // Convert general links
+ markdown =
markdown.replace(/<a\s+href=['"]([^'"]*?)['"][^>]*>(.*?)<\/a>/gi, '[$2]($1)');
+
+ // Process {@link} tags for Javadoc-style links
+ markdown = processJuneauLinks(markdown);
+
+ // Convert Java trees to custom elements (must come before
convertClassHierarchies)
+ markdown = convertJavaTreeToCustomElements(markdown);
+
+ // Convert class hierarchy trees
+ markdown = convertClassHierarchies(markdown);
+
+ // Convert tables (basic support)
+ markdown = convertTables(markdown);
+
+ // Clean up whitespace and stray characters
+ markdown = markdown.replace(/\n\s*\n\s*\n/g, '\n\n'); // Multiple newlines
to double
+ markdown = markdown.replace(/^\s*\|\s*/gm, ''); // Remove stray | prefixes
+
+ // Fix quote issues in JSON strings (common conversion artifact)
+ markdown = markdown.replace(/"\\"([^"]*)\\""/g, '"$1"'); // Fix escaped
quotes in JSON
+ markdown = markdown.replace(/"\\"([^"]*)"([^"]*)\\""/g, '"$1"$2"'); // Fix
partial escapes
+
+ // IMPROVED: Escape curly braces that might be interpreted as JSX
expressions (but not in code blocks)
+ // Handle nested braces by escaping ALL individual braces outside code
blocks
+ markdown = markdown.replace(/[{}]/g, (match, offset, string) => {
+ // Check if we're inside a code block by counting ``` markers before
this position
+ const beforeMatch = string.substring(0, offset);
+ const codeBlockMarkers = (beforeMatch.match(/```/g) || []).length;
+
+ // If odd number of ``` markers, we're inside a code block - don't
escape
+ if (codeBlockMarkers % 2 === 1) {
+ return match; // Keep original curly braces in code blocks
+ }
+
+ // Escape individual braces outside code blocks
+ return '\\' + match;
+ });
+
+ // Remove leading whitespace, but preserve indentation in code blocks
+ markdown = markdown.replace(/^\s+/gm, (match, offset, string) => {
+ const beforeMatch = string.substring(0, offset);
+ const codeBlockMarkers = (beforeMatch.match(/```/g) || []).length;
+
+ // If odd number of ``` markers, we're inside a code block - preserve
indentation
+ if (codeBlockMarkers % 2 === 1) {
+ return match; // Keep original indentation in code blocks
+ }
+
+ // Remove leading whitespace outside code blocks
+ return '';
+ });
+ markdown = markdown.replace(/[ \t]+$/gm, ''); // Trailing whitespace on
lines
+ markdown = markdown.trim();
+
+ // Add frontmatter
+ const frontmatter = `---
+title: "${title}"
+---
+
+`;
+
+ return frontmatter + markdown;
+}
+
+function convertCodeBlocks(content) {
+ // Look for code patterns that span multiple lines with | prefix (from
DocGenerator)
+ content = content.replace(/(\|[\s\S]*?)(?=\n\n|\n[^|]|$)/g, (match) => {
+ // Check if this looks like code (contains Java keywords, semicolons,
etc.)
+ if (match.includes('public ') || match.includes('private ') ||
match.includes('class ') ||
+ match.includes('import ') || match.includes('return ') ||
match.includes('void ') ||
+ match.includes(';') || match.includes('new ') ||
match.includes('@Override') ||
+ match.includes('<jk>') || match.includes('<js>') ||
match.includes('<jc>')) {
+
+ // Remove the | prefix (used for HTML indentation) and convert to
code block
+ let code = match.replace(/^\|\s*/gm, '').trim();
+
+ // Process Juneau tags in code
+ code = code.replace(/<jk>(.*?)<\/jk>/gi, '$1'); // keywords
+ code = code.replace(/<js>(.*?)<\/js>/gi, '$1'); // strings
+ code = code.replace(/<jc>(.*?)<\/jc>/gi, '$1'); // comments
+ code = code.replace(/<ja>(.*?)<\/ja>/gi, '$1'); // annotations
+ code = code.replace(/<jf>(.*?)<\/jf>/gi, '$1'); // fields
+ code = code.replace(/<jsm>(.*?)<\/jsm>/gi, '$1'); // static methods
+ code = code.replace(/<jm>(.*?)<\/jm>/gi, '$1'); // methods
+ code = code.replace(/<jv>(.*?)<\/jv>/gi, '$1'); // variables
+ code = code.replace(/<jp>(.*?)<\/jp>/gi, '$1'); // parameters
+
+ return '\n```java\n' + code + '\n```\n';
+ }
+ return match; // Not code, leave as is
+ });
+
+ return content;
+}
+
+function processJuneauLinks(content) {
+ const javadocBaseUrl = '../apidocs';
+
+ function expandPackageAbbreviations(className) {
+ for (const [abbrev, fullPackage] of
Object.entries(packageAbbreviations)) {
+ if (className.startsWith(abbrev + '.')) {
+ return className.replace(abbrev + '.', fullPackage + '.');
+ }
+ }
+ return className;
+ }
+
+ // Process {@link package.Class#method method} patterns
+ content =
content.replace(/\{@link\s+([a-zA-Z0-9_.]+)#([a-zA-Z0-9_()]+)(?:\s+([^}]+))?\}/g,
(match, className, method, displayText) => {
+ const expandedClass = expandPackageAbbreviations(className);
+ const classPath = expandedClass.replace(/\./g, '/');
+ const display = displayText ||
`${className.split('.').pop()}#${method}`;
+ return `[${display}](${javadocBaseUrl}/${classPath}.html#${method})`;
+ });
+
+ // Process {@link package.Class Class} patterns
+ content = content.replace(/\{@link\s+([a-zA-Z0-9_.]+)(?:\s+([^}]+))?\}/g,
(match, className, displayText) => {
+ const expandedClass = expandPackageAbbreviations(className);
+ const classPath = expandedClass.replace(/\./g, '/');
+ const display = displayText || className.split('.').pop();
+ return `[${display}](${javadocBaseUrl}/${classPath}.html)`;
+ });
+
+ return content;
+}
+
+function convertClassHierarchies(content) {
+ // Look for class hierarchy patterns in HTML and convert to markdown with
CSS classes
+ content =
content.replace(/<ul\s+class=['"](?:.*\s)?javatree(?:\s.*)?['"][^>]*>([\s\S]*?)<\/ul>/gi,
(match, listContent) => {
+ let converted = '\n<div class="javatree">\n\n';
+
+ // Convert list items with class attributes
+ listContent =
listContent.replace(/<li\s+class=['"]([^'"]*?)['"][^>]*>(.*?)<\/li>/gi, (match,
className, content) => {
+ // Clean up nested HTML
+ content = content.replace(/<[^>]*>/g, '');
+ content = content.trim();
+ return `- <li class="${className}">${content}</li>`;
+ });
+
+ converted += listContent;
+ converted += '\n\n</div>\n';
+ return converted;
+ });
+
+ return content;
+}
+
+function convertJavaTreeToCustomElements(content) {
+ // Convert Java tree HTML structures to custom element format
+ console.log('🔍 convertJavaTreeToCustomElements called, content length:',
content.length);
+
+ const result =
content.replace(/<ul\s+class=['"](?:.*\s)?javatree(?:\s.*)?['"][^>]*>([\s\S]*?)<\/ul>/gi,
(match, listContent) => {
+ try {
+ console.log('Converting Java tree:', match.substring(0, 100) +
'...');
+ const lines = parseJavaTreeToLines(listContent, 0);
+ return '\n' + lines.join('\n') + '\n';
+ } catch (error) {
+ console.error('Error converting Java tree:', error);
+ return match;
+ }
+ });
+
+ console.log('🔍 convertJavaTreeToCustomElements finished');
+ return result;
+}
+
+function parseJavaTreeToLines(html, depth) {
+ const lines = [];
+ const depthPrefix = '>'.repeat(depth);
+
+ // Parse properly nested <li> elements
+ let pos = 0;
+ while (pos < html.length) {
+ const liMatch =
html.substring(pos).match(/<li\s+class=['"]([^'"]*?)['"][^>]*>/);
+ if (!liMatch) break;
+
+ const fullLiStart = pos + liMatch.index;
+ const className = liMatch[1];
+ const liOpenEnd = fullLiStart + liMatch[0].length;
+
+ // Find the matching </li> tag, accounting for nested <li> elements
+ let liEnd = findMatchingLiClose(html, liOpenEnd);
+ if (liEnd === -1) {
+ // If no closing </li> found, take until next <li> or end
+ const nextLi =
html.substring(liOpenEnd).search(/<li\s+class=['"][^'"]*['"][^>]*>/);
+ liEnd = nextLi !== -1 ? liOpenEnd + nextLi : html.length;
+ }
+
+ const content = html.substring(liOpenEnd, liEnd).trim();
+
+ // Extract the main content before any nested <ul>
+ const ulIndex = content.indexOf('<ul');
+ const mainContent = ulIndex !== -1 ? content.substring(0,
ulIndex).trim() : content.trim();
+
+ // Process {@link} tags and convert to markdown links
+ const processedContent = processJavaTreeContent(mainContent);
+
+ // Map class names to custom elements
+ const elementType = mapClassToElement(className);
+
+ // Handle javatreec (condensed) lists
+ if (content.includes("class='javatreec'") ||
content.includes('class="javatreec"')) {
+ const condensedItems = extractCondensedItems(content);
+ if (condensedItems.length > 0) {
+ const condensedLine = depthPrefix + '>' +
condensedItems.join(' ');
+ lines.push(condensedLine);
+ pos = liEnd + 5; // Skip past </li>
+ continue;
+ }
+ }
+
+ // Add the main element
+
lines.push(`${depthPrefix}<${elementType}>${processedContent}</${elementType}>`);
+
+ // Process nested <ul> elements
+ const nestedUls = content.match(/<ul[^>]*>([\s\S]*?)<\/ul>/gi);
+ if (nestedUls) {
+ nestedUls.forEach(nestedUl => {
+ const nestedContent = nestedUl.replace(/^<ul[^>]*>/,
'').replace(/<\/ul>$/, '');
+ const nestedLines = parseJavaTreeToLines(nestedContent, depth
+ 1);
+ lines.push(...nestedLines);
+ });
+ }
+
+ pos = liEnd + 5; // Skip past </li>
+ }
+
+ return lines;
+}
+
+function findMatchingLiClose(html, startPos) {
+ let depth = 1;
+ let pos = startPos;
+
+ while (pos < html.length && depth > 0) {
+ const nextLiOpen = html.substring(pos).search(/<li[^>]*>/);
+ const nextLiClose = html.substring(pos).search(/<\/li>/);
+
+ if (nextLiClose === -1) return -1;
+
+ if (nextLiOpen !== -1 && nextLiOpen < nextLiClose) {
+ depth++;
+ pos += nextLiOpen + 4;
+ } else {
+ depth--;
+ if (depth === 0) {
+ return pos + nextLiClose;
+ }
+ pos += nextLiClose + 5;
+ }
+ }
+
+ return -1;
+}
+
+function mapClassToElement(className) {
+ const mapping = {
+ 'jac': 'java-abstract-class',
+ 'jc': 'javac-class',
+ 'jic': 'java-interface',
+ 'ja': 'java-annotation',
+ 'je': 'java-enum',
+ 'jm': 'java-method',
+ 'jmp': 'java-method-private',
+ 'jma': 'java-method-annotation',
+ 'jf': 'java-field',
+ 'jfp': 'java-field-private'
+ };
+
+ return mapping[className] || 'java-class';
+}
+
+function processJavaTreeContent(content) {
+ // Process {@link} tags
+ content = content.replace(/\{@link\s+([^}]+)\}/gi, (match, linkContent) =>
{
+ const parts = linkContent.split(/\s+/);
+ const target = parts[0];
+ const text = parts.slice(1).join(' ') || target.split('.').pop();
+
+ // Expand oaj abbreviation
+ const expandedTarget = target.replace(/^oaj\./, 'org.apache.juneau.');
+ const linkPath = expandedTarget.replace(/\./g, '/');
+
+ return `[${text}](../apidocs/${linkPath}.html)`;
+ });
+
+ // Process <c> tags (convert to backticks)
+ content = content.replace(/<c>(.*?)<\/c>/gi, '`$1`');
+
+ // Process <jk> tags (Java keywords)
+ content = content.replace(/<jk>(.*?)<\/jk>/gi, '$1');
+
+ // Clean up HTML entities
+ content = content.replace(/</g, '<').replace(/>/g,
'>').replace(/ /g, ' ');
+
+ // Remove extra whitespace
+ content = content.replace(/\s+/g, ' ').trim();
+
+ return content;
+}
+
+function extractCondensedItems(content) {
+ const items = [];
+ const javatreecMatch =
content.match(/<ul\s+class=['"]javatreec['"][^>]*>([\s\S]*?)<\/ul>/i);
+
+ if (javatreecMatch) {
+ const innerContent = javatreecMatch[1];
+ const liMatches =
innerContent.match(/<li\s+class=['"]([^'"]*?)['"][^>]*>([\s\S]*?)<\/li>/gi);
+
+ if (liMatches) {
+ liMatches.forEach(liMatch => {
+ const classMatch =
liMatch.match(/class=['"]([^'"]*?)['"][^>]*>/);
+ const contentMatch = liMatch.replace(/^<li[^>]*>/,
'').replace(/<\/li>$/, '');
+
+ if (classMatch) {
+ const className = classMatch[1];
+ const elementType = mapClassToElement(className);
+ const processedContent =
processJavaTreeContent(contentMatch);
+
items.push(`<${elementType}>${processedContent}</${elementType}>`);
+ }
+ });
+ }
+ }
+
+ return items;
+}
+
+function parseJavaTreeNode(html) {
+ const nodes = [];
+
+ // Clean up the HTML and normalize it
+ let normalizedHtml = html.trim();
+
+ // Split by <li> tags and process each
+ const liParts =
normalizedHtml.split(/<li\s+class=['"]([^'"]*?)['"][^>]*>/i);
+
+ // Skip the first empty part
+ for (let i = 1; i < liParts.length; i += 2) {
+ const className = liParts[i];
+ let content = liParts[i + 1] || '';
+
+ // Find the end of this li content (next <li> or end of string)
+ const nextLiIndex =
content.search(/<li\s+class=['"][^'"]*['"][^>]*>/i);
+ if (nextLiIndex !== -1) {
+ content = content.substring(0, nextLiIndex);
+ }
+
+ const node = parseJavaTreeItem(className, content);
+ if (node) {
+ nodes.push(node);
+ }
+ }
+
+ return nodes;
+}
+
+function parseJavaTreeItem(className, content) {
+ try {
+ // Extract the main text/link for this item
+ const { name, link, type } = extractNodeInfo(className, content);
+
+ const node = {
+ name: name,
+ type: type,
+ link: link
+ };
+
+ // Look for nested <ul> elements for children
+ const children = [];
+ const nestedUlPattern =
/<ul(?:\s+class=['"]([^'"]*?)['"])?[^>]*>([\s\S]*?)<\/ul>/gi;
+ let ulMatch;
+
+ while ((ulMatch = nestedUlPattern.exec(content)) !== null) {
+ const ulClass = ulMatch[1] || '';
+ const ulContent = ulMatch[2];
+
+ if (ulClass.includes('javatreec')) {
+ // This is a condensed children list
+ const condensedChildren = parseCondensedChildren(ulContent);
+ if (condensedChildren.length > 0) {
+ // Find the last child that can have condensed children
+ if (children.length > 0) {
+ const lastChild = children[children.length - 1];
+ lastChild.childrenCondensed = true;
+ lastChild.children = (lastChild.children ||
[]).concat(condensedChildren);
+ } else {
+ // Apply to current node
+ node.childrenCondensed = true;
+ node.children = (node.children ||
[]).concat(condensedChildren);
+ }
+ }
+ } else {
+ // Regular nested children
+ const nestedChildren = parseJavaTreeNode(ulContent);
+ children.push(...nestedChildren);
+ }
+ }
+
+ if (children.length > 0) {
+ node.children = children;
+ }
+
+ return node;
+
+ } catch (error) {
+ console.error('Error parsing Java tree item:', error);
+ return null;
+ }
+}
+
+function parseCondensedChildren(html) {
+ const children = [];
+ const liPattern = /<li\s+class=['"]([^'"]*?)['"][^>]*>([\s\S]*?)<\/li>/gi;
+ let match;
+
+ while ((match = liPattern.exec(html)) !== null) {
+ const className = match[1];
+ const content = match[2];
+
+ const { name, link, type } = extractNodeInfo(className, content);
+ children.push({
+ name: name,
+ type: type,
+ link: link
+ });
+ }
+
+ return children;
+}
+
+function extractNodeInfo(className, content) {
+ // Map CSS class to our type system
+ const typeMap = {
+ 'jac': 'java-abstract-class',
+ 'jc': 'java-class',
+ 'jic': 'java-interface',
+ 'ja': 'java-annotation',
+ 'je': 'java-enum',
+ 'jm': 'java-method',
+ 'jf': 'java-field',
+ 'jmp': 'java-private-method',
+ 'jfp': 'java-private-field',
+ 'jma': 'java-annotation-method'
+ };
+
+ const type = typeMap[className] || 'java-class';
+
+ // Extract the main content before any nested <ul> tags
+ const mainContent = content.split('<ul')[0].trim();
+
+ // Extract link and name from {@link} patterns
+ const linkPattern = /\{@link\s+([^}]+)\}/gi;
+ const linkMatch = linkPattern.exec(mainContent);
+
+ let name = '';
+ let link = '';
+
+ if (linkMatch) {
+ const linkTarget = linkMatch[1];
+
+ // Handle package abbreviations
+ let expandedTarget = linkTarget;
+ for (const [abbrev, fullPackage] of
Object.entries(packageAbbreviations)) {
+ expandedTarget = expandedTarget.replace(new
RegExp(`^${abbrev}\\.`, 'g'), `${fullPackage}.`);
+ }
+
+ // Extract class name and method
+ const parts = expandedTarget.split('#');
+ const classPath = parts[0];
+ const methodName = parts[1];
+
+ // Get the simple class name
+ const classSegments = classPath.split('.');
+ name = classSegments[classSegments.length - 1];
+
+ // If there's a method, format it properly
+ if (methodName) {
+ // Clean up method signature from content after the {@link}
+ const afterLink = mainContent.replace(/\{@link[^}]+\}/, '').trim();
+ const codeMatch = afterLink.match(/<c>(.*?)<\/c>/);
+ let returnType = codeMatch ? codeMatch[1] : '';
+
+ // Decode HTML entities in return type
+ returnType = returnType.replace(/</g, '<').replace(/>/g,
'>').replace(/&/g, '&');
+
+ // Extract the method signature (everything after the return type)
+ const methodText = afterLink.replace(/<c>.*?<\/c>/,
'').replace(/ /g, ' ').trim();
+ let methodSig = methodText || methodName;
+
+ // Decode HTML entities in method signature
+ methodSig = methodSig.replace(/</g, '<').replace(/>/g,
'>').replace(/&/g, '&');
+
+ if (returnType) {
+ name = `${returnType} ${methodSig}`;
+ } else {
+ name = methodSig;
+ }
+ }
+
+ // Convert to documentation link
+ const classPathWithSlashes = classPath.replace(/\./g, '/');
+ link = `../apidocs/${classPathWithSlashes}.html`;
+ if (methodName) {
+ // Clean up method name for anchor - just the method name without
params
+ const cleanMethodName = methodName.split('(')[0];
+ link += `#${cleanMethodName}`;
+ }
+ } else {
+ // Fallback: extract text content
+ name = mainContent.replace(/<[^>]*>/g, '').replace(/ /g, '
').replace(/\s+/g, ' ').trim();
+ // Decode HTML entities
+ name = name.replace(/</g, '<').replace(/>/g,
'>').replace(/&/g, '&');
+ }
+
+ return { name, link, type };
+}
+
+function generateClassHierarchyComponent(nodes) {
+ const indent = ' ';
+
+ function nodeToString(node, depth = 1) {
+ const baseIndent = indent.repeat(depth);
+ let result = `${baseIndent}{ name: "${escapeQuotes(node.name)}", type:
"${node.type}"`;
+
+ if (node.link) {
+ result += `, link: "${node.link}"`;
+ }
+
+ if (node.childrenCondensed) {
+ result += `, childrenCondensed: true`;
+ }
+
+ if (node.children && node.children.length > 0) {
+ result += `,\n${baseIndent} children: [\n`;
+
+ const childStrings = node.children.map(child =>
nodeToString(child, depth + 2));
+ result += childStrings.join(',\n');
+
+ result += `\n${baseIndent} ]`;
+ }
+
+ result += ' }';
+ return result;
+ }
+
+ const nodeStrings = nodes.map(node => nodeToString(node));
+
+ return `<ClassHierarchy nodes={[\n${nodeStrings.join(',\n')}\n]} />`;
+}
+
+function escapeQuotes(text) {
+ return text.replace(/"/g, '\\"').replace(/'/g, "\\'");
+}
+
+function convertTables(content) {
+ // Convert HTML tables to Markdown tables
+ content = content.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (match,
tableContent) => {
+ let rows = [];
+
+ // Extract table rows
+ const rowMatches = tableContent.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi);
+ if (!rowMatches) return match; // Keep original if no rows found
+
+ let isHeaderProcessed = false;
+ let lastCategoryCell = '';
+
+ rowMatches.forEach((row, index) => {
+ // Extract cells from each row
+ const cellMatches = row.match(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi);
+ if (cellMatches) {
+ let cells = [];
+ let categoryFound = false;
+
+ cellMatches.forEach(cell => {
+ // Check for rowspan attribute (indicates category cell)
+ const rowspanMatch = cell.match(/rowspan=['"](\d+)['"]/i);
+ const hasRowspan = rowspanMatch !== null;
+
+ // Remove HTML tags but preserve structure for lists
+ let content = cell.replace(/<\/?t[hd][^>]*>/gi, '');
+
+ // Convert links - extract just the text content
+ content = content.replace(/<a[^>]*>([\s\S]*?)<\/a>/gi,
'$1');
+
+ // Convert nested lists to simple text with bullet points
+ content = content.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi,
(match, listContent) => {
+ const items =
listContent.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
+ const listItems = items.map(item => {
+ let text = item.replace(/<\/?li[^>]*>/gi,
'').replace(/<[^>]*>/g, '').trim();
+ return `• ${text}`;
+ });
+ return listItems.join('<br>');
+ });
+
+ // Clean up remaining HTML tags
+ content = content.replace(/<[^>]*>/g, '');
+ content = content.replace(/\s+/g, ' ').trim();
+
+ // Handle empty cells
+ if (!content) content = ' ';
+
+ if (hasRowspan && !categoryFound) {
+ // This is a category cell with rowspan
+ lastCategoryCell = content;
+ categoryFound = true;
+ cells.push(content);
+ } else {
+ cells.push(content);
+ }
+ });
+
+ // If this row doesn't have a category cell, use the last one
+ if (!categoryFound && lastCategoryCell && index > 0) {
+ cells.unshift(lastCategoryCell);
+ }
+
+ // Add the row
+ if (cells.length > 0) {
+ rows.push('| ' + cells.join(' | ') + ' |');
+ }
+
+ // Add header separator after first row if it contains <th>
tags
+ if (!isHeaderProcessed && row.includes('<th')) {
+ const separator = '| ' + cells.map(() => '---').join(' |
') + ' |';
+ rows.push(separator);
+ isHeaderProcessed = true;
+ }
+ }
+ });
+
+ return rows.length > 0 ? '\n\n' + rows.join('\n') + '\n\n' : match;
+ });
+
+ return content;
+}
+
+function convertFile(inputFile, outputFile) {
+ try {
+ const htmlContent = fs.readFileSync(inputFile, 'utf8');
+ const markdown = convertHtmlToMarkdown(htmlContent, inputFile);
+
+
+ // Ensure output directory exists
+ const outputDir = path.dirname(outputFile);
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ fs.writeFileSync(outputFile, markdown, 'utf8');
+ console.log(`✅ Converted: ${inputFile} -> ${outputFile}`);
+ return true;
+ } catch (error) {
+ console.error(`❌ Error converting ${inputFile}:`, error.message);
+ return false;
+ }
+}
+
+// CLI usage
+if (require.main === module) {
+ const args = process.argv.slice(2);
+ if (args.length < 2) {
+ console.log('Usage: node html-to-md-converter.js <input.html>
<output.md>');
+ process.exit(1);
+ }
+
+ const [inputFile, outputFile] = args;
+ convertFile(inputFile, outputFile);
+}
+
+module.exports = { convertHtmlToMarkdown, convertFile };
diff --git a/juneau-docs-poc/src/plugins/remark-version-replacer.js
b/juneau-docs-poc/src/plugins/remark-version-replacer.js
index 7891c825a..1176aa227 100644
--- a/juneau-docs-poc/src/plugins/remark-version-replacer.js
+++ b/juneau-docs-poc/src/plugins/remark-version-replacer.js
@@ -11,10 +11,10 @@
* specific language governing permissions and limitations under the License.
*/
-const { visit } = require('unist-util-visit');
+// No AST traversal needed - we use string replacement instead
/**
- * Simple string replacement function as fallback
+ * Simple string replacement function that handles all placeholder replacements
*/
function replaceInString(content, version, apiDocsUrl) {
return content
@@ -24,71 +24,35 @@ function replaceInString(content, version, apiDocsUrl) {
/**
* Remark plugin to replace version and API docs placeholders with actual
values.
- * This works inside code blocks and anywhere else in the markdown.
+ * Simple string replacement approach that works on the entire file content.
*/
function remarkVersionReplacer(options = {}) {
const version = options.version || '9.0.1';
const apiDocsUrl = options.apiDocsUrl || '../apidocs';
+ console.log(`🔧 remarkVersionReplacer initialized with version: ${version},
apiDocsUrl: ${apiDocsUrl}`);
+
return (tree, file) => {
- // First, do a string-level replacement on the entire file content
+ console.log(`📄 Processing file: ${file.path || 'unknown'}`);
+
+ // Replace placeholders in the entire file content before parsing
if (file.contents) {
- file.contents = replaceInString(file.contents, version, apiDocsUrl);
- }
- // Process all nodes that might contain text content
- visit(tree, (node) => {
- // Handle text nodes
- if (node.type === 'text' && node.value) {
- node.value = node.value.replace(/\{\{JUNEAU_VERSION\}\}/g, version);
- node.value = node.value.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
- }
-
- // Handle code nodes
- if (node.type === 'code' && node.value) {
- node.value = node.value.replace(/\{\{JUNEAU_VERSION\}\}/g, version);
- node.value = node.value.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
- }
+ const originalContent = file.contents;
+ const hasApiDocs = originalContent.includes('{{API_DOCS}}');
+ const hasVersion = originalContent.includes('{{JUNEAU_VERSION}}');
- // Handle inline code nodes
- if (node.type === 'inlineCode' && node.value) {
- node.value = node.value.replace(/\{\{JUNEAU_VERSION\}\}/g, version);
- node.value = node.value.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
+ if (hasApiDocs || hasVersion) {
+ console.log(`🎯 Found placeholders in ${file.path || 'unknown'}:
API_DOCS=${hasApiDocs}, VERSION=${hasVersion}`);
}
- // Handle link nodes
- if (node.type === 'link' && node.url) {
- node.url = node.url.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
- }
-
- // Handle HTML/JSX nodes (like our custom components)
- if (node.type === 'html' && node.value) {
- node.value = node.value.replace(/\{\{JUNEAU_VERSION\}\}/g, version);
- node.value = node.value.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
- }
+ file.contents = replaceInString(file.contents, version, apiDocsUrl);
- // Handle MDX JSX elements
- if (node.type === 'mdxJsxTextElement' || node.type ===
'mdxJsxFlowElement') {
- // Process children of JSX elements
- if (node.children) {
- node.children.forEach(child => {
- if (child.type === 'text' && child.value) {
- child.value = child.value.replace(/\{\{JUNEAU_VERSION\}\}/g,
version);
- child.value = child.value.replace(/\{\{API_DOCS\}\}/g,
apiDocsUrl);
- }
- });
- }
-
- // Process attributes
- if (node.attributes) {
- node.attributes.forEach(attr => {
- if (attr.value && typeof attr.value === 'string') {
- attr.value = attr.value.replace(/\{\{JUNEAU_VERSION\}\}/g,
version);
- attr.value = attr.value.replace(/\{\{API_DOCS\}\}/g, apiDocsUrl);
- }
- });
- }
+ const stillHasPlaceholders = file.contents.includes('{{API_DOCS}}') ||
file.contents.includes('{{JUNEAU_VERSION}}');
+ if (stillHasPlaceholders) {
+ console.log(`⚠️ WARNING: Still has unreplaced placeholders in
${file.path || 'unknown'}`);
}
- });
+ }
+ // No need for AST traversal since string replacement handles all cases
};
}