How to export block data while preserving the formatting

How do I get the data out of a block while preserving the formatting (as much as possible)?

This project aims to answer the question “how can I save the results of a logseq query?”

By leveraging the fact that your graph is rendered as HTML, you can have access to your formatted data. That includes query results with tables, advanced queries with custom views, markdown tables, lists, etc.

Functionality scope
Copy the formatted content of a single block to your clipboard via a button click.

Limitations
If your block has embedded images the formatting isn’t going to be pretty.

Disclaimer
I’d consider this project more a proof-of-concept than finished product. Do with it what you want, as it’s released under the AGPL v3 license.

Demonstration
What to expect
Video showing table and list results rendered in formatted plain-text and as HTML elements.

Details

  • This project uses kits
  • It offers the ability to export your data by adding a kit button as a child block to the parent with content you want to export.
  • The project script “exports” the block data to your clipboard.
    • If you paste the data to a text editor, the data will be in formatted plain-text.
    • If you paste the data to a rich-text editor, the data will render as HTML elements.

How to install

  1. Have the logseq kits framework installed, which this project uses.
  2. Create a new page named exportBlockContent, which will contain this project’s javascript code.
  3. Add a new javascript markdown code fence to the page exportBlockContent
  4. Include the following code within the new code fence
logseq.kits.exportBlockContent = exportBlockContent;

async function createHTMLContentWithImages(thisElement, keepOriginal = false) {
  const element = thisElement;
  if (!element || !(element instanceof Element)) {
    return null;
  }
  const htmlContent = element.cloneNode(true);
  
  if (keepOriginal) {
    // Return the original HTML without processing images
    return htmlContent.outerHTML;
  }
  
  // Process all images within the cloned content
  const images = htmlContent.querySelectorAll('img');
  for (const img of images) {
    try {
      // Create a temporary container for the image
      const tempContainer = document.createElement('div');
      tempContainer.appendChild(img.cloneNode());
      document.body.appendChild(tempContainer);
      
      // Use html2canvas to capture the image
      const canvas = await html2canvas(tempContainer, {
        logging: false,
        useCORS: true,
        backgroundColor: null
      });
      
      // Convert canvas to data URI
      const dataUri = canvas.toDataURL('image/png');
      
      // Replace original src with data URI
      img.src = dataUri;
      
      // Clean up temporary container
      document.body.removeChild(tempContainer);
    } catch (error) {
      console.error('Error processing image:', error);
    }
  }
  
  return htmlContent.outerHTML;
}

async function exportBlockContent(el, mode = 'process') {
  const me = event.target.closest('.ls-block');
  const parentBlock = me.parentElement.closest('.ls-block');
  
  try {
    let htmlContent;
    let clipboardData;

    switch (mode) {
      case 'original':
        htmlContent = await createHTMLContentWithImages(parentBlock, true);
        clipboardData = {
          'text/html': new Blob([htmlContent], { type: 'text/html' })
        };
        break;
      case 'plaintext':
        htmlContent = await createHTMLContentWithImages(parentBlock, true);
        clipboardData = {
          'text/plain': new Blob([htmlContent], { type: 'text/plain' })
        };
        break;
      default: // 'process'
        htmlContent = await createHTMLContentWithImages(parentBlock, false);
        clipboardData = {
          'text/html': new Blob([htmlContent], { type: 'text/html' }),
          'text/plain': new Blob([parentBlock.innerText], { type: 'text/plain' })
        };
    }

    // Use Electron's clipboard API if available
    if (window.electron && window.electron.clipboard && window.electron.clipboard.writeHTML) {
      window.electron.clipboard.writeHTML(htmlContent);
      console.log('Content copied to clipboard successfully using Electron API');
    } else {
      // Fallback to web API if Electron API is not available
      await navigator.clipboard.write([
        new ClipboardItem(clipboardData)
      ]);
      console.log('Content copied to clipboard successfully using Web API');
    }
  } catch (error) {
    console.error('Failed to copy content to clipboard:', error);
  }
}
exportBlockContent(null)
  1. Add a kit button {{runpage exportBlockContent}} as a child to the block with content that you want to export. For example:
- Example table
  | item | content |
  |---|---|
  | 1 | stuff |
	- {{runpage exportBlockContent}}
  • Nice!
  • Rather than calling runpage every time, should provide a macro for a dedicated button that runs only one function, e.g.:
    :copyparent "<button class='kit eval' data-kit='exportblockcontent'> copy content of parent block </button>"
    
1 Like