How to automatically process every row of a table

I’m trying to figure out how I can automatically process every row of a table in Obsidian. Example: given the following table

Day Count
Monday 5
Tuesday 7
Wednesday 15

I want to automatically create a note for each row of the table above like:


Day:: Monday
Count:: 7


So, for this specific example, you could regard it as the reverse functionality of Dataview.

Googling for a suitable plugin, I found Stefan Wolfrum’s @metawops Table to CSV Exporter that is already really near to what I’m looking for but tailored for one very specific kind of processing: putting each row into a newly created CSV file.

If there is nothing more general yet, I could take the “Table to CSV Exporter” as a start point and change the “row processing part” for my own functionality. But I was hoping there could be an already existing solution to my problem. Like a plugin that can read each row of a table, put each column value of the row into a configurable variable and hand it over to QuickAdd? Or any other way?

Thank you for your ideas and suggestions in advance!

1 Like

What is the origin of your table? Is it entered manually, or is it based on a query of some sorts?

In my current scenario, it is entered manually.

OK, just thinking out loud here. There are three distinct operations as I see it:

  • Retrieve the rows of the table
  • Process the rows
  • Create the new files based on the input

All of these should be doable given some javascript knowledge, and with the help of either dataviewjs or Templater.

Retrieve the rows of the table

Two options spring to mind. Either you select and copy the table to the clipboard before triggering a Templater template using tp.system.clipboard() to process it (or you could possibly also use tp.file.selection()).

Alternatively, you could use some query selector to locate a table in the current document, and process it from there. This can be done from both dataviewjs and Templater.

Process the rows

A little depending on how you get the rows, the amount of processing will vary, but given you collect it from the clipboard it’s a matter of splitting into lines, and then splitting each line based on tab characters. Easy enough, I think.

Then you should have the wanted data in a readily available format. What else do you need to get you going on creating the new file? Do you need a date of sorts, or will you pull that out the magic hat somehow?

If you’re doing the query selector, it could be a little trickier to either navigate the markdown (somewhat easy) or the html (somewhat harder). Still doable, but not as elegant as the clipboard variant.

Create the new files

Using Templater you would have access to its file functions, whilst using dataviewjs you would rely on the javascript functions. They do work, but maybe they’ve got as many bells and whistles as the Templater alternatives.

The files you want created seems simple enough, so either option should be good, and just a matter of personal preferences.


All in all, I think it boils down to some personal preference and how familiar you’re with javascript and/or the various API of the plugins. It should be rather doable within the context of both Templater and dataviewjs, and personally I’m leaning towards the Templater option and copying the data into the clipboard before triggering the template to create the files.

The question I don’t know your thoughts about would be what will be the names of the new files, and how are you going to pick those names. What are the criteria for the file names?

1 Like

So here is a rough sketch of a template which creates new files based on the table:

<%*
const activeFile = tp.config.active_file
console.log("activeFile: ", activeFile)
// const activeFilePath = activeFile.path
const activeFolder = activeFile.parent.path
const baseName = activeFile.basename

const lines = (await tp.system.clipboard()).split("\n")
for (let line of lines) {
  const [day, count] = line.split("\t")
  
  if ( day === "Day" ) 
    continue; // Ignore header line

  // Process the data into content and a new file name
  console.log(day, "->", count)
  const content = `Day:: ${ day }\nCount:: ${ count }\n`
  const myFilename = baseName + "-" + day

  // Write the new file
  console.log(myFilename, content)
  await tp.file.create_new(
    content, myFilename, 
    false, this.app.vault.getAbstractFileByPath(activeFolder))
}

-%>

How to use this template, after setting up a hotkey for triggering the template:

  • Select the table contents (with or without header line) from the reading view, and copy to clipboard
  • Hit the hotkey to insert the template above

Watch and see how you now got three extra files name after the note with the table, with the day appended, and the content you wanted. Of course, you’ll need to find a better way to name your files than this, but it shows the gist of how it can be done.

1 Like

Thank you very much for your ideas, thoughts and the sketch, @holroy!

After reading all your suggestions, I tend to join your conclusion that it’s a good option to use some Templater code blocks for this purpose. For the skeleton (getting the table out of the note and iterating through the lines of the table), I prefer to adapt the approach of Table to CSV Explorer (here^1 and here^2):

const view = app.workspace.getActiveViewOfType(MarkdownView);
const html = view.previewMode.containerEl;
const table = html.querySelector("table");

From here, we have all rows in a cleanly structured form and can iterate over them:

var rows = table.rows;
for (var i = 0; i < rows.length; i++) {
   var row = [];

   var cols = rows[i].querySelectorAll("td, th");
   for (var j = 0; j < cols.length; j++) {
      var cellContent = (cols[j] as HTMLElement).innerText;
      row.push(cellContent);
   }

   processRow(row);
}

The specific processing of the rows is up to the definition of the function processRow:

function processRow(row) {
   // do whatever you need to do with the row
   // e.g. create a new file for each row
}

Although, all the row processing could be coded by hand in the function processRow, I still hope, there is a way to put the cells of a row into QuickAdd variables and to trigger QuickAdd (within of processRow), so the existing functionality of QuickAdd (including creation of new notes/files and the usage of Templater templates) could be reused.

I’m not too familiar with QuickAdd, yet, so this does indeed sound like an interesting option. I’m looking for something familiar to have one macro/suggestet select a given file, and then use another macro/template/capture to produce stuff related to the chosen file.

Will keep a lookout on this topic, and respond if I find something helpful for either of our cases.

1 Like

There are a way using the api, and I hopefully can do a mockup later on tonight. I’m just wondering what kind of file names are you imagening? Or how do you think guiding QuickAdd to choose file names.

I’ve also seen you can do a execute QuickAdd choice through the api, is that how you intended the workflow? That is let processRow call that api?

1 Like

Thank you for your hints and questions. That helped me to find a solution a lot!

In the end, it came down to the following solution based solely on QuickAdd:

  1. For the actual processing of the data of individual rows, create a QuickAdd choice of type Capture or Template. This choice will be called automatically for each row. It should contain and use variables which will be set to the data of a row. The variable will be set from “outside”, i.e. by caller of the choice.
  2. Create a QuickAdd macro that triggers the JavaScript given below.
  3. Create a QuickAdd choice of type Macro triggering the macro created above. The QuickAdd choice created in this step will be triggered manually upon a note containing a table. That’s why it might be helpful to bind this QuickAdd choice to a keyboard shortcut (by activating the thunderbolt icon).

Following is the source code of the JavaScript file to use for the QuickAdd macro in the step 2 above (adjust the QuickAdd variable names accordingly to your QuickAdd choice from the step 1 above):

module.exports = async (params) => {
    const { quickAddApi, obsidian, app } = params;
    const view = app.workspace.getActiveViewOfType(obsidian.MarkdownView);
    const html = view.previewMode.containerEl;
    const table = html.querySelector("table");
    
    var rows = table.rows;
    for (var i = 0; i < rows.length; i++) {
        var row = [];

        var cols = rows[i].querySelectorAll("td, th");
        for (var j = 0; j < cols.length; j++) {
            var cellContent = cols[j].innerText;
            row.push(cellContent);
        }

        if (i == 0) {
            await processHeaderRow(row);
        } else {
            await processDataRow(row);
        }
    }

    async function processHeaderRow(row) {
        console.log("Header row: " + row);
        /* extend this function according to your needs */
    }

    async function processDataRow(row) {
        console.log("Data row: " + row);
        /* tweak the contents of this function according to your needs */
        let filename = await quickAddApi.inputPrompt("Confirm file name:", row[0], row[0])
        await quickAddApi.executeChoice("CreateNewFileFromVariables", {
            /* adjust this name accordligly to your QuickAdd variable */
            "filename": filename,

            /* adjust this name accordligly to your QuickAdd variable */
            "myQuickAddChoiceVar1": row[0],

            /* adjust this name accordligly to your QuickAdd variable */
            "myQuickAddChoiceVar2": row[2]

            /* Maybe you need further variables. Then extend appropriately.... */
        });
    }
}
1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.