@ 261106.11
current
// Copyright 2026 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import express from 'express';
import fs from 'fs';
import path from 'path';
import { marked } from 'marked';
import { Graphviz } from '@hpcc-js/wasm-graphviz';

const app = express();
const PORT = 8081;

app.use(express.json());

// Resolve paths
const __dirname = path.resolve(); // Cwd is docs/viewer
const docsDir = path.resolve(__dirname, '..');
const distDir = path.resolve(__dirname, 'dist');

console.log('Loading Graphviz WASM...');
const g = await Graphviz.load();

function resolvePath(currentFile, relativePath) {
  if (relativePath.startsWith('/')) return relativePath.slice(1);

  const parts = currentFile.split('/');
  parts.pop(); // Remove filename

  const relParts = relativePath.split('/');
  for (const part of relParts) {
    if (part === '.') continue;
    if (part === '..') parts.pop();
    else parts.push(part);
  }
  return parts.join('/');
}

// Helper to render markdown to HTML with Graphviz
async function renderMarkdown(filePath, relPath) {
  let content = fs.readFileSync(filePath, 'utf-8');

  // Handle Graphviz blocks
  const codeBlockRegex = /```(?:graphviz|dot)\n([\s\S]*?)\n```/g;
  let match;
  const replacements = [];

  while ((match = codeBlockRegex.exec(content)) !== null) {
    let dot = match[1];
    const firstBrace = dot.indexOf('{');
    if (firstBrace !== -1) {
      dot = dot.slice(0, firstBrace + 1) +
        '\n    bgcolor="transparent";' +
        '\n    graph [fontname="Courier"];' +
        '\n    node [fontname="Courier"];' +
        '\n    edge [fontname="Courier"];' +
        dot.slice(firstBrace + 1);
    }

    try {
      const svg = g.layout(dot, "svg", "dot");
      const modifiedSvg = svg.replaceAll('font-family="Courier,monospace"', 'font-family="JetBrains Mono"');
      replacements.push({
        original: match[0],
        replacement: `<div class="graphviz">${modifiedSvg}</div>`
      });
    } catch (e) {
      console.error(`Failed to render graphviz:`, e);
      replacements.push({
        original: match[0],
        replacement: `<div class="graphviz"><pre>${e.message}</pre></div>`
      });
    }
  }

  for (const r of replacements) {
    content = content.replace(r.original, r.replacement);
  }

  let html = await marked.parse(content);

  // Rewrite links
  html = html.replaceAll(/href="([^"]+)"/g, (match, href) => {
    if (href.startsWith('http')) return match; // Skip external links

    const resolvedPath = resolvePath(relPath, href);

    if (href.startsWith('src/') || href.startsWith('include/') ||
        resolvedPath.startsWith('src/') || resolvedPath.startsWith('include/')) {

      const finalPath = href.startsWith('src/') || href.startsWith('include/') ? href : resolvedPath;
      return `href="https://source.chromium.org/chromium/chromium/src/+/main:v8/${finalPath}" target="_blank"`;
    }

    if (href.endsWith('.md')) {
      return `href="${href.replace(/\.md$/, '.html')}"`;
    }

    return match;
  });

  // Convert inline code paths to links
  html = html.replaceAll(/<code>((?:src|include)\/[^<]+)<\/code>/g, '<a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/$1" target="_blank"><code>$1</code></a>');

  return html;
}

// Serve static files for the viewer itself
app.use('/viewer', express.static(__dirname));

// Handle requests for processed HTML files
app.get('*', async (req, res, next) => {
  const relPath = req.path.slice(1); // Remove leading slash

  if (!relPath.endsWith('.html')) {
    return next();
  }

  const htmlPath = path.join(distDir, relPath);
  const mdPath = path.join(docsDir, relPath.replace(/\.html$/, '.md'));

  try {
    if (fs.existsSync(htmlPath)) {
      res.sendFile(htmlPath);
    } else if (fs.existsSync(mdPath)) {
      console.log(`Generating on the fly: ${relPath}`);
      const html = await renderMarkdown(mdPath, relPath);
      res.send(html);
    } else {
      res.status(404).send('File not found');
    }
  } catch (error) {
    res.status(500).send(`Error: ${error.message}`);
  }
});

// Handle comments API
const commentsFile = path.resolve(__dirname, 'comments.json');

function readComments() {
  if (fs.existsSync(commentsFile)) {
    try {
      return JSON.parse(fs.readFileSync(commentsFile, 'utf-8'));
    } catch (e) {
      return {};
    }
  }
  return {};
}

app.get('/api/comments', (req, res) => {
  const file = req.query.file;
  const comments = readComments();
  res.json(comments[file] || {});
});

app.post('/api/comments', (req, res) => {
  const { file, blockIndex, text } = req.body;

  if (!file || blockIndex === undefined || !text) {
    return res.status(400).send('Bad Request: Missing fields');
  }

  const comments = readComments();

  if (!comments[file]) comments[file] = {};
  const blockIndexStr = String(blockIndex);
  if (!comments[file][blockIndexStr]) comments[file][blockIndexStr] = [];

  comments[file][blockIndexStr].push({
    text,
    timestamp: new Date().toISOString()
  });

  fs.writeFileSync(commentsFile, JSON.stringify(comments, null, 2));
  res.json({ success: true });
});

// Default route serves index.html
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
  console.log(`Viewer available at http://localhost:${PORT}/viewer/`);
});