kevboh / longform

A plugin for Obsidian that helps you write and edit novels, screenplays, and other long projects.
Other
610 stars 30 forks source link

Step Request: include block by name #109

Open cheezopath opened 1 year ago

cheezopath commented 1 year ago

first of all thanks for a great plugin!

I like to write my scenes in levels of detail depending what stage I'm at;

All my scenes are built from the same template, with a heading for each stage. I'd love to be able to select which stage to compile. How difficult would it be to have the option to include only a specified heading name per scene in the compile?

I managed to get an example user script working, but I'm totally new to .js at the moment as far as customising the script goes. Does this functionality make sense to include in the default suite of scripts? Is it easy to implement?

kevboh commented 1 year ago

Does this functionality make sense to include in the default suite of scripts?

Eventually, yes, probably. Filtering/excluding parts of notes seems in-scope for default steps.

Is it easy to implement?

Right now, sort of, but not trivially. The correct way to make this achievable would be to figure out how to expose the structural meaning of the markdown (what programmers would call the AST) to steps; that doesn't happen right now, but it would make writing this sort of step far easier.

In the short term you could probably get away with a custom step that uses a regex to match the content you want, then return that content from the step. It's less learning javascript than figuring out the regex. If you use templates to get the same format for every note then figuring out the regex to match a part of that note shouldn't be too bad; once you have the regex, the step would look something like:

// See docs for `input` and `context` types and expected return type.
async function compile(input, context) {
  const regex = ... // your regex here
  return input.map((sceneInput) => {
    // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
    // for more on getting the results of a regex (e.g. if you use named groups)
    const contents = sceneInput.contents.match(regex)[0];
    return {
      ...sceneInput,
      contents
    };
  });
}

module.exports = {
  description: {
    name: "Select Block by Header",
    description: "Selects a block of text from a hardcoded regex",
    availableKinds: ["Scene"],
    options: []
  },
  compile: compile
};

Note a few things about the above example:

  1. I didn't write the regex. You can use a tool like regex101 to figure yours out.
  2. The regex is hardcoded. You can add step options and figure out how to dynamically form the regex if, for example, you wanted to be able to enter the header to grab text from in the step UI itself.
  3. This runs per-scene.

Hope this helps. If you get something flexible feel free to PR it!

cheezopath commented 1 year ago

Thanks for the tips! I managed to put together this:

module.exports = {
    description: {
        name: "Select Block by Header",
        description: "Selects a matching heading 1 block per scene",
        availableKinds: ["Scene"],
        options: [
            {
                id: "heading",
                name: "Heading to include",
                description: "Uses this string to look for a matching level 1 heading block to include in compile",
                type: "Text",
                default: "draft",
            },
        ],
    },

    compile (input, context) {
        const heading = context.optionValues["heading"];
        const re = new RegExp("(?<=^# "+heading+"\n)(.*?\n)*?(?=^# )\n*","gm")
        input = input.map(i => {
        i.contents = i.contents.match(re);
        return i;
      });
        return input;
    }
};

It will include, per scene only the h1 block that matches the given string, and only if using the "#" heading style. It seems to work well just before a "Prepend Title" step.

It would not at all surprise me if there is a much more elegant way to do this, and the AST approach you mentioned would be great in future, but I'm happy to work out how to do a PR with this if you think it's worth it. In any case, that's the proof of concept :)