Save as HTML and Print articles from Civitai Articles (Modern Style)

Save Civitai articles as HTML or Print them with modern, readable styling.

// ==UserScript==
// @name        Save as HTML and Print articles from Civitai Articles (Modern Style)
// @version     1.4
// @namespace   cyberdelia extract
// @description Save Civitai articles as HTML or Print them with modern, readable styling.
// @license     MIT
// @match       https://civitai.com/articles/*
// @icon        https://civitai.com/favicon.ico
// @author      Cyberdelia
// @grant       none
// ==/UserScript==

(function () {
  'use strict';

  // Updated author selector to match either known patterns.
  const authorSelector =
    'div.mantine-Text-root.mantine-Text-gradient.mantine-1pr2ftc, div.mantine-Text-root.mantine-7c8fqu';
  const titleSelector =
    'h1.mantine-Text-root.mantine-Title-root.mantine-6porwj';
  const articleContentSelector = 'article';

  // Modern CSS style for the output (used for HTML file and print preview).
  const additionalStyles = `
    /* Base container styles for exported output */
    body {
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
      line-height: 1.6 !important;
      color: #333 !important;
      background-color: #fff !important;
      padding: 20px !important;
      margin: 0 auto !important;
      max-width: 800px !important;
    }
    h1 {
      font-size: 1.8em !important;
      margin-bottom: 0.5em !important;
      color: #222 !important;
    }
    h2, h3, h4, h5, h6 {
      color: #222 !important;
      margin-top: 1.2em !important;
      margin-bottom: 0.5em !important;
    }
    p {
      margin-bottom: 1em !important;
      color: #333 !important;
    }
    hr {
      border: none !important;
      border-top: 1px solid #ccc !important;
      margin: 1.5em 0 !important;
    }
    a {
      color: #228be6 !important;
      text-decoration: none !important;
    }
    a:hover {
      text-decoration: underline !important;
    }
    img {
      max-width: 100% !important;
      height: auto !important;
    }
    /* Force all text within the export container to be dark */
    .export-container, .export-container * {
      color: #333 !important;
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
    }
  `;

  // Set up a MutationObserver to (re‑)insert our buttons if removed.
  function setupObserver() {
    const observer = new MutationObserver(() => {
      const article = document.querySelector(articleContentSelector);
      // Only add the buttons if the article exists and our container isn't already present.
      if (article && !document.querySelector('.button-container')) {
        addButton(article);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  // Helper function to extract title and author from the page.
  function getMetaInfo() {
    const titleElement = document.querySelector(titleSelector);
    const authorElement = document.querySelector(authorSelector);
    const title = titleElement ? titleElement.textContent.trim() : "Untitled";
    const author = authorElement ? authorElement.textContent.trim() : "Unknown Author";
    return { title, author };
  }

  // Build the full HTML output using our modern style.
  function buildOutputHTML(articleContent) {
    const { title, author } = getMetaInfo();
    return `
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>${title}</title>
    <style>${additionalStyles}</style>
  </head>
  <body>
    <div class="export-container">
      <h1>${title}</h1>
      <p>by ${author}</p>
      <hr>
      ${articleContent}
    </div>
  </body>
</html>`;
  }

  // Save the article as an HTML file.
  function saveAsHTML(article) {
    const articleContent = article ? article.innerHTML : '';
    const htmlContent = buildOutputHTML(articleContent);
    const { title } = getMetaInfo();
    const filename =
      title.replace(/[^\w\s]/gi, '_').replace(/\s+/g, '_') + '.html';
    const blob = new Blob([htmlContent], { type: 'text/html' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = filename;
    link.click();
  }

  // Print the article.
  function printContent(article) {
    const articleContent = article ? article.innerHTML : '';
    const printWindow = window.open('', '_blank');
    if (!printWindow) {
      alert('Popup blocker may be preventing printing');
      return;
    }
    printWindow.document.write(buildOutputHTML(articleContent));
    printWindow.document.close();
    printWindow.print();
  }

  // Create and insert the button container (with two buttons).
  function addButton(article) {
    const container = document.createElement('div');
    container.className = 'button-container';

    const htmlButton = document.createElement('button');
    htmlButton.textContent = 'Save HTML';
    htmlButton.addEventListener('click', () => saveAsHTML(article));

    const printButton = document.createElement('button');
    printButton.textContent = 'Print';
    printButton.addEventListener('click', () => printContent(article));

    container.appendChild(htmlButton);
    container.appendChild(printButton);

    // Insert custom styles for the button container (only once).
    if (!document.getElementById('custom-button-style')) {
      const style = document.createElement('style');
      style.id = 'custom-button-style';
      style.textContent = `
        .button-container {
          margin-bottom: 10px;
          padding: 10px;
          background-color: #f7f7f7;
          border: 2px solid #228be6;
          border-radius: 8px;
          text-align: center;
          box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
        }
        .button-container button {
          color: white;
          background-color: #228be6;
          height: 40px;
          border: none;
          border-radius: 4px;
          font-weight: bold;
          font-size: medium;
          cursor: pointer;
          padding: 0 1.5rem;
          margin: 5px;
          white-space: nowrap;
          overflow: hidden;
          transition: background-color 0.2s ease;
        }
        .button-container button:hover {
          background-color: #1a7bb8;
        }
      `;
      document.head.appendChild(style);
    }

    if (article.parentNode) {
      article.parentNode.insertBefore(container, article);
    }
  }

  setupObserver();
})();