Tricky dataviewjs query to list desperate quotes from different titles with the same Class, Author

Found this GEM here https://www.reddit.com/r/ObsidianMD/submit

This code enables you to search for all in body text tagged with #quote. It’s great!

Can anyone help me customise this… I’m so lost when it comes to dataviewjs. 'd like to search according to property fields and list a header and underlying text.

Example:

Class = References

Author = Jane Doe

Title = Janes Book of Fish

Quotes from {{title}}

  1. Quote number one
  2. Quote number two

How can I alter the code below to search for Class field, Author field, Title field and then list the quotes I have listed in desperate reference notes?

I might have five references where Jane Doe is an author (or co-author). I want to list the quotes from each title in a single note that’s probably titled [[Jane Doe Quotes]]. The code will produce the following:

Quotes from Janes Book of Fish

  1. Quote number one
  2. Quote number two

Quotes from Janes Doe Memoir

  1. Quote number one
  2. Quote number two
  3. Quote number three

Quotes from The Mysterious Deaths of Jane and John Doe

  1. Quote number one
  2. Quote number two

If you’re able to provide some help that would be so greatly appreciated!

Thanks and stay classy Santiago

const pagesWithQuotes = await Promise.all(
  dv.pages("#quote").map(
    ({ file }) =>
      new Promise(async (resolve, reject) => {
        const content = await dv.io.load(file.path);
        resolve({
          file,
          content,
        });
      })
  )
);

// Create an array of quote objects, each containing:
// {
//   quote: string, // the quote text itself
//   file: File, // a reference to the file containing the quote
//   maybeBlockId: string, // the block ID at the end of the quote, if it exists
//   maybeNearestParentHeading: string // the nearest parent heading, if it exists
// }
const quotes = pagesWithQuotes
  .map(({ file, content }) => ({
    file,
    quotes: content
      // Split into lines
      .split("\n")

      // Remove any bullets or preceding whitespace. Separate
      // out quote and the block ID, if a block ID exists.
      .map(
        (content) =>
          /\s*(\s|\*|\-)*\s*(?<quote>.*?)\s*?(?<maybeBlockId>\^[0-9a-zA-Z\-]{0,15})?$/gm.exec(
            content
          )?.groups || {}
      )

      // Find the nearest previous heading, if one exists
      .map(({ quote, maybeBlockId }, lineNumberZeroIndexed) => ({
        quote,
        maybeBlockId,
        maybeNearestParentHeading: /\n#{1,6}\s(.*)/gm
          .exec(content.split("\n").slice(0, lineNumberZeroIndexed).join("\n"))
          ?.last(),
      }))

      // Only return lines with the "#quote" tag in them
      .filter(({ quote }) => quote.includes("#quote"))

      // Remove the "#quote" tag from each quote string
      .map(({ quote, maybeBlockId, maybeNearestParentHeading }) => ({
        quote: quote.replace("#quote", "").trim(),
        maybeBlockId,
        maybeNearestParentHeading,
      }))

      // Filter out empty values (this would happen
      // if a line contained only the text "#quote")
      .filter(({ quote }) => Boolean(quote)),
  }))
  .reduce((accumulator, { file, quotes }) => {
    quotes.forEach(({ quote, maybeBlockId, maybeNearestParentHeading }) =>
      accumulator.push({ quote, file, maybeBlockId, maybeNearestParentHeading })
    );
    return accumulator;
  }, []);

const getLinkForQuote = ({ file, maybeBlockId, maybeNearestParentHeading }) => {
  if (maybeBlockId) {
    // Block ID exists. Return a link to the block ID.`[[${file.path}#${maybeBlockId}|${file.name}]]`);
    return `[[${file.path}#${maybeBlockId}|${file.name}]]`;
  } else if (maybeNearestParentHeading) {
    // A parent heading exists. Return a link to the heading.
    return `[[${file.path}#${maybeNearestParentHeading}|${file.name}]]`;
  } else {
    // Fallback to linking to the page
    return String(file.link);
  }
};

quotes.forEach(({ quote, file, maybeBlockId, maybeNearestParentHeading }) => {
  dv.paragraph(
    `${quote} (${getLinkForQuote({
      file,
      maybeBlockId,
      maybeNearestParentHeading,
    })})`
  );
});

As you said, that query (here’s the correct link) retrieves the quotes using #quote instead of using inline fields.

Now that you want to include Class, Author and Title I wonder how you are storing that info?

Are you using inline fields? Like:

- [quote:: I'm a quote. ] [author:: Jane Doe] [title:: Janes Book of Fish] [class:: references]

Do you have notes by book?

Janes book of fish

  • I’m a quote. Jane Doe. #references

If you can provide an example of how you store your quotes it will be easier to help you to understand how to write the query.

Hi, thanks for your reply.

Class and Author are YAML frontmatter properties and title is just the title (this.file.link). Then #Quote is used in the text block I want to display.

So I’m wanting to search across References Class, Author Name where tag is placed in text.

Title: Janes Book of Fish

Here is a quote to send to dataviewjs search #Quote

EDIT: For future reference, please check the updated version of the examples in this post.

Let’s assume you have a few notes of quotes per book in a folder “quotes”. For example:

(Download this example files Quotes.zip (1.4 KB) and copy to your vault.)

# /Quotes/Janes Book of Fish.md
---
author: Jane Doe
class: references
---

# Quotes
- One lorem #quote 
- Two lorem #quote 
#/Quotes/Janes Doe Memoir.md
---
author: Jane Doe
class: references
---

# Quotes
- Three lorem #quote 
- Four lorem #quote 
#/Quotes/The Mysterious Deaths of Jane and John Doe.md
---
author: 
- Jane Doe
- John Doe
class: mistery
---

# Quotes
- Five lorem #quote 
- Six lorem #quote 

#/Quotes/John Doe Memoir.md
---
author: John Doe
class: references
---

# Quotes
- Seven lorem #quote 
- Eight lorem #quote 

Given that structure we can easily pull all the quotes containing #quote with:

```dataview
LIST 
	file.lists.text
FROM 
	"Quotes"
WHERE 
	contains(file.lists.text, "#quote")
```

Output:

Now if we want to get all the notes with class references we can use:

```dataview
LIST 
	replace(file.lists.text, "#quote", "")
FROM 
	"Quotes"
WHERE 
	contains(file.lists.text, "#quote")
AND 
	class = "references"
```

Notice that we included replace(file.lists.text, "#quote", "") to output the quote without the #quote

Output:

And if we want to list all the notes class = mistery and author = Jane doe, we can do:

```dataview
LIST 
	replace(file.lists.text, "#quote", "")
FROM 
	"Quotes"
FLATTEN 
	author as A
WHERE 
	contains(file.lists.text, "#quote")
AND 
	class = "mistery"
AND 
	A = "Jane Doe"
```

Here, given that author is a list type property with multiple values we need to flatten the author field to filter Jane’s Doe quotes.

Output:

Take in consideration that this structure is just an example based on what I understand is your way to store quotes, there are a few drawbacks like the author and class properties belongs to the book and no the quote, and that you need to add the quotes in a list, but at least it gives you an idea or how to create a query with multiple parameters.

Does that work for you?

1 Like

This is absolutely 10000000% more amazing a reply than I ever thought possible. Thank you, that’s extremely helpful, works just as I have hoped and I appreciate you taking the time.

I have two other questions from this. When I remove the dot points the search no longer works - is there a simple way to make this search across dot point lists as well as standard text?

And, is there anyway to remove the need for tags and query a heading title for it and it’s text below? I’m thinking along the lines of the following, but as a dataview query.

![[The Mysterious Deaths of Jane and John Doe#Quotes]]

Thanks again!!!

The entire premise of this solution is to use lists to get to the quotes. You can’t get the text outside of lists or tasks. (That is with an exception related to inline fields, which would require a different approach)

One question I’ve got for @sanyaissues though, is how will you query behave if there are list items that don’t have the #quote tag? And if you treat all list items as quotes, do you then really need to tag them with #quote ? And to you @Greener, is it likely that your list items could contain something other than quotes? The solution to this could be to filter out and display only the list items being tagged correctly.

Still working with lists, you are able to extract the section heading for a given list, so you could have all of your quotes within one section, i.e. “Quotes”, and then pull out just the list items from that section. This can be done using meta(item.section).subpath filtering on the lists.

This would both remove the need for the quote tags, and at the same time allow other lists in other sections to co-exist without interfering with the quotes section.

You have a fair point there! Indeed, if lists include items with another #tag or no tags at all, the query will output those list items too. My bad! Instead of using file.lists.text we can make it work with file.lists.tags.

And yes, if we assume the list only contains quotes, filtering by #quotes it has no use. I’m keeping it in the example just to show how to filter by tags.

Here’s the example fixed, for future reference.

Sample Notes

Notes

Quotes.zip (1.5 KB)

# /Quotes/Janes Book of Fish.md

---
author: Jane Doe
class: references
---

# Quotes
- One lorem #quote 
- Two lorem #quote 
- This is an idea #idea
- This is a text without a tag

#/Quotes/Janes Doe Memoir.md
---
author: Jane Doe
class: references
---

# Quotes
- Three lorem #quote 
- Four lorem #quote 
#/Quotes/John Doe Memoir.md
---
author: John Doe
class: references
---

# Quotes
- Seven lorem #quote 
- Eight lorem #quote
#/Quotes/The Mysterious Deaths of Jane and John Doe.md
---
author: 
- Jane Doe
- John Doe
class: mistery
---

# Quotes
- Five lorem #quote 
- Six lorem #quote 
- Another item
- Amazing idea #idea

List of all the quotes

Query
```dataview
LIST
	WITHOUT ID
	L.text
FROM 
	"Quotes"
FLATTEN
	file.lists AS L
WHERE
	contains(L.tags, "quote")
```
Output

Quotes grouped by title without the #:

Query
```dataview
LIST 
	replace(rows.L.text, "#quote", "")
FROM 
	"Quotes"
FLATTEN
	file.lists AS L
WHERE
	contains(L.tags, "quote")
GROUP BY 
	file.link
Output

Quotes by class:

Query
```dataview
LIST 
	replace(rows.L.text, "#quote", "")
FROM 
	"Quotes"
FLATTEN
	file.lists AS L
WHERE
	class = "references"
AND
	contains(L.tags, "quote")
GROUP BY 
	file.link
```
Output

Quotes from mistery class and Jane Doe author:

Query
```dataview
LIST 
	replace(rows.L.text, "#quote", "")
FROM 
	"Quotes"
FLATTEN 
	author AS A
FLATTEN
	file.lists AS L
WHERE
	A = "Jane Doe"
AND
	class = "mistery"
AND
	contains(L.tags, "quote")
GROUP BY
	file.link
```
Output

@Greener this is also a good suggestion, you can just include something like this in your book notes:

# Quotes
- First quote 
- Second quote

And then use a query like this to pull ONLY the list below the # Quotes section.

```dataview
LIST
	L.text
FLATTEN 
	file.lists as L
WHERE 
	meta(L.section).subpath = "Quotes"
```

The queries which has been presented so far are not dataviewjs queries, which reads through the entire file content, and I believe this is a better approach in this case.

But are there any particular reasons why you asked for a dataviewjs solution? Or was that just the starting point where you thought it would be the better option?

@sanyaissues , just a little nitpicking to help your queries go from good to even better. In the “author and class” query, when you do FLATTEN on both author and file.lists, we could end up with many rows. Imagine having 5 authors in a file, and 10 list items. This double FLATTEN would then result in 50 rows before the filter them down again. In most cases this is not an issue, but it’s something worth taking into account if you want to FLATTEN multiple lists; How many rows do we possibly extend our query with? An alternate solution would be to work with the authors and lists as lists a little longer, use filter() to limit them down, and then flatten to get the visual result we want.

1 Like

@holroy It took me a while to figure out how to filter by author and class (given that those properties could be single value items or lists) and then filter by quote without FLATTEN lists.

Still, I wasn’t able to use filter() in file.list.tags. I was trying something like
filter(file.lists.tags, (L) => contains(L, "quote")) but looks like tags are a list inside another list. So I didn’t figure out how to access them (without FLATTEN). Any tips?

Query:

```dataview
LIST filter(file.lists, (L) => contains(L.text, "quote")).text
FROM
	"Quotes"
WHERE 
	filter(list(author), (L) => contains(L, "Jane Doe"))
AND 
	filter(list(class), (L) => contains(L, "mistery"))
```

Output

The Mysterious Deaths of Jane and John Doe:
- Five lorem #quote
- Six lorem #quote

If you got some property as single values (not a list with one item) or as a list with any number of items, you can use the following to ensure that it’s a list: flat(list(propertyName)). This does two things, first of all the list() makes whatever comes next into a list, so this makes a single value into a list, and a list into a list of lists. That second part is not ideal, as that creates that double list you’re talking about, but the flat() command flattens out the list to be just one level, and then we’ve arrived at the list we wanted.

So without building a proper test set, the following query is untested, but should hopefully work for checking both author and class (no matter whether they’re single values or lists:

```dataview
LIST WITHOUT ID L.text
FROM "Quotes"
FLATTEN file.lists as L
WHERE contains(L.tags, "quote")
WHERE filter( flat(list(author)), (author) => contains(author, "Jane Doe"))
  AND filter( flat(list(class)), (class) => contains(class, "mistery"))
```
1 Like

Now that you mention it, it’s so obvious. flat() was what I was looking for in my example. Thanks!

```dataview
LIST filter(flat(file.lists.tags), (L) => L = "#quote")
FROM
	"Quotes"
WHERE 
	filter(list(author), (L) => contains(L, "Jane Doe"))
AND 
	filter(list(class), (L) => contains(L, "mistery"))
	
```

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