Extract to specified heading with Note Composer

Use case or problem

I often want to use Note Composer to merge/extract into a new heading within an existing heading within a new or existing note.

Proposed solution

Let it be possible for Note Composer to merge/extract into a new heading within an existing heading within a new or existing note.

Current workaround

As a partial workaround, you can create the heading first within the source. Then you can select the content as well as the heading to be extracted.

Related feature requests

Related help thread looking for a Templater solution: Extract selection to heading

Thanks so much!

4 Likes

I’d also like to “Merge entire file into” a heading.

Surprisingly, the Note Refactor community plugin can only partly do what you’re asking.

I wonder if the QuickAdd plugin might be able to do it, but am too lazy to dig thru its documentation.

As a workaround on Mac/iOS you do it with Shortcuts and the Shortcut Launcher plugin (tho doing it really nicely would probably get a bit complicated).

It occurred to me that maybe we can type a heading after the note name when selecting the note to extract to (as when linking to a heading), but that doesn’t work.

3 Likes

This script is extremely close to accomplishing this request. Instead of appending the text within a heading within a chosen heading in a destination note, it appends it at the end. And it works with blocks and headings! @AlanG is a magician! Here’s the link: Templater: open file in new tab and append to end of file - #11 by AlanG

Thanks!

1 Like

Using @AlanG’s script, but exchanging the note suggester with a faster method (my vault is too large):

<%*
// By AlanG; Taken from: https://share.note.sx/tus76cc6

// Get the selected text
let text = app.workspace.activeEditor.getSelection();

// Get the destination file
const files = app.vault.getMarkdownFiles().map(file => {
  const fileCache = app.metadataCache.getFileCache(file);
  if (fileCache?.frontmatter?.aliases) {
    file.display = `${file.basename}\n${fileCache.frontmatter.aliases.join(", ")}`;
  } else {
    file.display = file.basename;
  }
  return file;
});

const destination = (await tp.system.suggester(item => item.display, files)).basename;

if (destination) {
  // Define the destination path
  const destinationPath = tp.file.find_tfile(`${destination}.md`).path;

  // Get the heading
  let embed;
  const heading = await tp.system.prompt('Enter the new heading or escape/cancel to use a block reference');
  if (heading) {
    text = `\n\n## ${heading.trim()}\n\n${text.trim()}\n\n`;
    embed = `![[${destinationPath}#${heading}]]\n`;
  } else {
    // Block reference
    const ref = Math.random().toString(36).slice(2, 7);
    text = `\n\n${text.trim()} ^${ref}\n\n`;
    embed = `![[${destinationPath}#^${ref}]]\n`;
  }

  // Add the text to the destination note
  await app.vault.adapter.append(destinationPath, text);

  // Replace the selection with an embedded link to the new location
  app.workspace.activeEditor.editor.replaceSelection(embed);
}
-%>
3 Likes

Very cool! I am noticing that your version of AlanGs script is adding .md within the transclusion, after the note name (before the #), so the embeds don’t work correctly. I’m sure you will find a fix. And I definitely can see how this optimization technique can be applied in many scripts. In this case I like the suggestions, but thanks for sharing this cool trick! I have a lot to learn. And thanks for all your helpful contributions on the forum! Great stuff!

1 Like

That’s great @gino_m - I didn’t know about getMarkdownFiles(). I’ve updated the script use that instead, and your addition of the aliases is very smooth :+1:

Rather than using tp.file.find_tfile, you could just keep the TFile as the selection from Templater, and use .path instead of .basename.

4 Likes
  • Was wrestling with that because first the target file was not found and created in the root folder.
    I re-copied your last solution to finalize now.
  • Was working for me, although I only tried the embed ID. Alan fixed by adding/leaving the slices. I seem to remember deleting those when I tried to make the path work with the other suggester.
  • I’m not a programmer, either, I do trial and error and try to get a very obstinate (and I daresay very stupid) robot to guide me (down to a place where people have lost all their hair).

I like my embed ID below the section, so it is:

  text = `\n\n${text.trim()} \n^${ref}\n\n`

for me.

2 Likes

I get this error from Templater when trying this:

Templater Error: Template parsing error, aborting. 
 fileCache.frontmatter.aliases.join is not a function

:thinking:

I guess sometimes aliases is not an array? :man_shrugging:

I’ve updated the script to take that into account:

2 Likes

Other related which, combined with your own and others, hopefully signal an interest in broader Note Composer functionality :slight_smile: :

1 Like

Number one is similar to what I am trying to solve using QuickAdd at the moment.

1 Like

Yep, that seems to have solved it. Thanks!

1 Like

I modified the script that @AlanG created so that it creates a bidirectional link between where the text is extracted to and from. I found this to be helpful in situations where you are extracting a lot of content to a note and you are having trouble returning to the source because the backlinks tab is overwhelming and of no use when trying to find backlinks for a specific block or heading (see Option to sort backlinks by heading/block-ids).

As is obvious by my modifications to AlanG’s code, I am definitely not experienced creating or even editing scripts. Fortunately, so far, based on my testing, the following appears to work.

//Modified from @AlanG’s awesome script at https://share.note.sx/tus76cc6
<%*
// Get the selected text
let text = app.workspace.activeEditor.getSelection()
const srcnote = tp.file.title

// Get the desired file - credits to https://forum.obsidian.md/t/67901/4
const files = app.vault.getMarkdownFiles().map(file => {
  const fileCache = app.metadataCache.getFileCache(file)
  file.display = file.basename
  if (fileCache?.frontmatter?.aliases) {
    if (Array.isArray(fileCache.frontmatter.aliases)) {
      file.display = `${file.basename}\n${fileCache.frontmatter.aliases.join(", ")}`
    } else if (typeof fileCache.frontmatter.aliases === 'string') {
      file.display = `${file.basename}\n${fileCache.frontmatter.aliases}`
    }
  }
  return file
})
const destination = (await tp.system.suggester(files.map(x => x.display), files, false, 'Start typing any note name...', 10)).path

// Get the heading
let embed
const backref = Math.random().toString(36).slice(2, 7)
const heading = await tp.system.prompt('Enter the new heading or escape/cancel to use a block reference')
if (heading) {
  text = `\n\n## ${heading.trim()}\n`+  "FROM—> [[" + srcnote + "#^" + backref + "]]" + `\n${text.trim()}\n\n`
  embed = "EXTRACTED ^" + backref +  `\n\n![[${destination.slice(0, -3)}#${heading}]]\n`
} else {
  // Block reference
  const ref = Math.random().toString(36).slice(2, 7)
  text = `\n`+  "FROM—> [[" + srcnote + "#^" + backref + "]]" +`\n${text.trim()} ^${ref}\n\n`
  embed = "EXTRACTED ^" + backref +`\n\n![[${destination.slice(0, -3)}#^${ref}]]\n`
}

// Add the text to the destination note
await app.vault.adapter.append(destination, text)

// Replace the selection with an embedded link to the new location
app.workspace.activeEditor.editor.replaceSelection(embed)
-%>

If the embeds appear wrong after running the script, try navigating away from them back into the current note. For me, this fixed any erroneous rendering of transclusions. I only tested on mobile, and am not sure this would technically be an Obsidian bug, since plugins are being utilized. Perhaps there is a way to fix this such as triggering some sort of reloading of the current note at the end of the script.

Anyways, use this modified version of AlanG’s script at your own risk.

Thanks!

Just to check - did you know there is a link icon at the top-right of each embed which takes you back to the source? You don’t need to use the backlinks panel. (Of course that icon doesn’t help if you want to see what the text link is at a glance.)

Sorry. My phrasing was unclear. By “source” I am referring to the note that contains the embed. So, to be able to quickly return to this source note and see the original context from which the text was extracted, I altered your script to leave just the word “EXTRACTED” and a block-id, two lines above the embed (adding it immediately above would cause the block links not to work). And in the destination note, I had the script add a line either immediately within the heading or immediately above the block reference with the word “FROM—>” and a link to the block-id that was generated above the embed in the “source” note.

Thanks again for everything! I can’t tell you how much you have helped me out with this script. Do you have a link to “buy you a coffee” or something?

1 Like

I know this is getting a bit excessive at this point, but my plan now is to try to modify the script to have an option to create and name a new note, then use a template from a list of a few choices to populate it, and then finally to do exactly what the script currently does. Not sure it’s possible, but it sure feels like it might be.

You can probably get everything you need from here:

2 Likes