obsidian#CachedMetadata TypeScript Examples

The following examples show how to use obsidian#CachedMetadata. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: file.ts    From Obsidian_to_Anki with GNU General Public License v3.0 6 votes vote down vote up
constructor(file_contents: string, path:string, url: string, data: FileData, file_cache: CachedMetadata) {
        this.data = data
        this.file = file_contents
        this.path = path
        this.url = url
        this.original_file = this.file
        this.file_cache = file_cache
        this.formatter = new FormatConverter(file_cache, this.data.vault_name)
    }
Example #2
Source File: files-manager.ts    From Obsidian_to_Anki with GNU General Public License v3.0 6 votes vote down vote up
async genAllFiles() {
        for (let file of this.files) {
            const content: string = await this.app.vault.read(file)
            const cache: CachedMetadata = this.app.metadataCache.getCache(file.path)
            const file_data = this.dataToFileData(file)
            this.ownFiles.push(
                new AllFile(
                    content,
                    file.path,
                    this.data.add_file_link ? this.getUrl(file) : "",
                    file_data,
                    cache
                )
            )
        }
    }
Example #3
Source File: collecting.ts    From obsidian-tracker with MIT License 6 votes vote down vote up
// Not support multiple targets
// In form 'key: value', target used to identify 'frontmatter key'
export function getDateFromFrontmatter(
    fileCache: CachedMetadata,
    query: Query,
    renderInfo: RenderInfo
): Moment {
    // console.log("getDateFromFrontmatter");
    // Get date from 'frontMatterKey: date'

    let date = window.moment("");

    let frontMatter = fileCache.frontmatter;
    if (frontMatter) {
        if (helper.deepValue(frontMatter, query.getTarget())) {
            let strDate = helper.deepValue(frontMatter, query.getTarget());

            // We only support single value for now
            if (typeof strDate === "string") {
                strDate = helper.getDateStringFromInputString(
                    strDate,
                    renderInfo.dateFormatPrefix,
                    renderInfo.dateFormatSuffix
                );

                date = helper.strToDate(strDate, renderInfo.dateFormat);
                // console.log(date);
            }
        }
    }

    return date;
}
Example #4
Source File: markdown-file.ts    From obsidian-dataview with MIT License 6 votes vote down vote up
/** Parse raw (newline-delimited) markdown, returning inline fields, list items, and other metadata. */
export function parseMarkdown(
    path: string,
    contents: string[],
    metadata: CachedMetadata
): { fields: Map<string, Literal[]>; lists: ListItem[] } {
    let fields: Map<string, Literal[]> = new Map();

    // Only parse heading and paragraph elements for inline fields; we will parse list metadata separately.
    for (let section of metadata.sections || []) {
        if (section.type == "list" || section.type == "ruling") continue;

        for (let lineno = section.position.start.line; lineno <= section.position.end.line; lineno++) {
            let line = contents[lineno];

            // Fast bail-out for lines that are too long or do not contain '::'.
            if (line.length > 2048 || !line.includes("::")) continue;
            line = line.trim();

            let inlineFields = extractInlineFields(line);
            if (inlineFields.length > 0) {
                for (let ifield of inlineFields) addRawInlineField(ifield, fields);
            } else {
                let fullLine = extractFullLineField(line);
                if (fullLine) addRawInlineField(fullLine, fields);
            }
        }
    }

    // Extract task data and append the global data extracted from them to our fields.
    let [lists, extraData] = parseLists(path, contents, metadata);
    for (let [key, values] of extraData.entries()) {
        if (!fields.has(key)) fields.set(key, values);
        else fields.set(key, fields.get(key)!!.concat(values));
    }

    return { fields, lists };
}
Example #5
Source File: editorFilter.fixture.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
function makeEditorFilter(
  filterText: string,
  displayText: string,
  matches: SearchMatches,
  score: number,
  metadata?: CachedMetadata,
): EditorFixtureFilter {
  const prepQuery = makePreparedQuery(filterText);
  const fuzzyMatch = makeFuzzyMatch(matches, score);
  const cachedMetadata = metadata ?? getCachedMetadata();

  return {
    inputText: `${editorTrigger}${filterText}`,
    displayText,
    prepQuery,
    fuzzyMatch,
    cachedMetadata,
  };
}
Example #6
Source File: file.ts    From Obsidian_to_Anki with GNU General Public License v3.0 5 votes vote down vote up
file_cache: CachedMetadata
Example #7
Source File: collecting.ts    From obsidian-tracker with MIT License 5 votes vote down vote up
// In form 'regex with value', name group 'value' from users
export function collectDataFromWiki(
    fileCache: CachedMetadata,
    query: Query,
    renderInfo: RenderInfo,
    dataMap: DataMap,
    xValueMap: XValueMap
): boolean {
    let links = fileCache.links;
    if (!links) return false;

    let searchTarget = query.getTarget();
    let searchType = query.getType();

    let textToSearch = "";
    let strRegex = searchTarget;

    // Prepare textToSearch
    for (let link of links) {
        if (!link) continue;

        let wikiText = "";
        if (searchType === SearchType.Wiki) {
            if (link.displayText) {
                wikiText = link.displayText;
            } else {
                wikiText = link.link;
            }
        } else if (searchType === SearchType.WikiLink) {
            // wiki.link point to a file name
            // a colon is not allowed be in file name
            wikiText = link.link;
        } else if (searchType === SearchType.WikiDisplay) {
            if (link.displayText) {
                wikiText = link.displayText;
            }
        } else {
            if (link.displayText) {
                wikiText = link.displayText;
            } else {
                wikiText = link.link;
            }
        }
        wikiText = wikiText.trim();

        textToSearch += wikiText + "\n";
    }

    return extractDataUsingRegexWithMultipleValues(
        textToSearch,
        strRegex,
        query,
        dataMap,
        xValueMap,
        renderInfo
    );
}
Example #8
Source File: collecting.ts    From obsidian-tracker with MIT License 5 votes vote down vote up
// No value, count occurrences only
export function collectDataFromFrontmatterTag(
    fileCache: CachedMetadata,
    query: Query,
    renderInfo: RenderInfo,
    dataMap: DataMap,
    xValueMap: XValueMap
): boolean {
    // console.log("collectDataFromFrontmatterTag");
    // console.log(query);
    // console.log(dataMap);
    // console.log(xValueMap);

    let frontMatter = fileCache.frontmatter;
    let frontMatterTags: string[] = [];
    if (frontMatter && frontMatter.tags) {
        // console.log(frontMatter.tags);
        let tagMeasure = 0.0;
        let tagExist = false;
        if (Array.isArray(frontMatter.tags)) {
            frontMatterTags = frontMatterTags.concat(frontMatter.tags);
        } else if (typeof frontMatter.tags === "string") {
            let splitted = frontMatter.tags.split(query.getSeparator(true));
            for (let splittedPart of splitted) {
                let part = splittedPart.trim();
                if (part !== "") {
                    frontMatterTags.push(part);
                }
            }
        }
        // console.log(frontMatterTags);
        // console.log(query.getTarget());

        for (let tag of frontMatterTags) {
            if (tag === query.getTarget()) {
                // simple tag
                tagMeasure = tagMeasure + renderInfo.constValue[query.getId()];
                tagExist = true;
                query.addNumTargets();
            } else if (tag.startsWith(query.getTarget() + "/")) {
                // nested tag
                tagMeasure = tagMeasure + renderInfo.constValue[query.getId()];
                tagExist = true;
                query.addNumTargets();
            } else {
                continue;
            }

            // valued-tag in frontmatter is not supported
            // because the "tag:value" in frontmatter will be consider as a new tag for each existing value

            let value = null;
            if (tagExist) {
                value = tagMeasure;
            }
            let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]);
            addToDataMap(dataMap, xValue, query, value);
            return true;
        }
    }

    return false;
}
Example #9
Source File: collecting.ts    From obsidian-tracker with MIT License 5 votes vote down vote up
// Not support multiple targets
// In form 'regex with value', name group 'value' from users
export function getDateFromWiki(
    fileCache: CachedMetadata,
    query: Query,
    renderInfo: RenderInfo
): Moment {
    //console.log("getDateFromWiki");
    // Get date from '[[regex with value]]'

    let date = window.moment("");

    let links = fileCache.links;
    if (!links) return date;

    let searchTarget = query.getTarget();
    let searchType = query.getType();

    for (let link of links) {
        if (!link) continue;

        let wikiText = "";
        if (searchType === SearchType.Wiki) {
            if (link.displayText) {
                wikiText = link.displayText;
            } else {
                wikiText = link.link;
            }
        } else if (searchType === SearchType.WikiLink) {
            // wiki.link point to a file name
            // a colon is not allowed be in file name
            wikiText = link.link;
        } else if (searchType === SearchType.WikiDisplay) {
            if (link.displayText) {
                wikiText = link.displayText;
            }
        } else {
            if (link.displayText) {
                wikiText = link.displayText;
            } else {
                wikiText = link.link;
            }
        }
        wikiText = wikiText.trim();

        let strRegex = "^" + searchTarget + "$";
        return extractDateUsingRegexWithValue(wikiText, strRegex, renderInfo);
    }

    return date;
}
Example #10
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 5 votes vote down vote up
cache: CachedMetadata;
Example #11
Source File: folder.ts    From obsidian-fantasy-calendar with MIT License 5 votes vote down vote up
cache: CachedMetadata;
Example #12
Source File: helpers.ts    From obsidian-checklist-plugin with MIT License 5 votes vote down vote up
getAllTagsFromMetadata = (cache: CachedMetadata): string[] => {
  if (!cache) return []
  const frontmatterTags = getFrontmatterTags(cache)
  const blockTags = (cache.tags ?? []).map((e) => e.tag)
  return [...frontmatterTags, ...blockTags]
}
Example #13
Source File: helpers.ts    From obsidian-checklist-plugin with MIT License 5 votes vote down vote up
getFrontmatterTags = (cache: CachedMetadata, todoTags: string[] = []) => {
  const frontMatterTags: string[] = parseFrontMatterTags(cache?.frontmatter) ?? []
  if (todoTags.length > 0) return frontMatterTags.filter((tag: string) => todoTags.includes(getTagMeta(tag).main))
  return frontMatterTags
}
Example #14
Source File: fileCachedMetadata.fixture.ts    From obsidian-switcher-plus with GNU General Public License v3.0 5 votes vote down vote up
export function getCachedMetadata(): CachedMetadata {
  return {
    links: getLinks(),
    embeds: getEmbeds(),
    tags: getTags(),
    headings: getHeadings(),
  };
}
Example #15
Source File: editorFilter.fixture.ts    From obsidian-switcher-plus with GNU General Public License v3.0 5 votes vote down vote up
leftMetadata: CachedMetadata = { tags: getTags() }
Example #16
Source File: import-impl.ts    From obsidian-dataview with MIT License 5 votes vote down vote up
export function runImport(
    path: string,
    contents: string,
    stats: FileStats,
    metadata: CachedMetadata
): Partial<PageMetadata> {
    return parsePage(path, contents, stats, metadata);
}
Example #17
Source File: markdown-file.ts    From obsidian-dataview with MIT License 5 votes vote down vote up
/** Extract markdown metadata from the given Obsidian markdown file. */
export function parsePage(path: string, contents: string, stat: FileStats, metadata: CachedMetadata): PageMetadata {
    let tags = new Set<string>();
    let aliases = new Set<string>();
    let fields = new Map<string, Literal[]>();
    let links: Link[] = [];

    // File tags, including front-matter and in-file tags.
    (metadata.tags || []).forEach(t => tags.add(t.tag.startsWith("#") ? t.tag : "#" + t.tag));

    // Front-matter file tags, aliases, AND frontmatter properties.
    if (metadata.frontmatter) {
        for (let tag of extractTags(metadata.frontmatter)) {
            if (!tag.startsWith("#")) tag = "#" + tag;
            tags.add(tag);
        }

        for (let alias of extractAliases(metadata.frontmatter) || []) aliases.add(alias);

        let frontFields = parseFrontmatter(metadata.frontmatter) as Record<string, Literal>;
        for (let [key, value] of Object.entries(frontFields)) addInlineField(key, value, fields);
    }

    // Links in metadata.
    for (let rawLink of metadata.links || []) {
        let parsed = EXPRESSION.embedLink.parse(rawLink.original);
        if (parsed.status) links.push(parsed.value);
    }

    // Merge frontmatter fields with parsed fields.
    let markdownData = parseMarkdown(path, contents.split("\n"), metadata);
    mergeFieldGroups(fields, markdownData.fields);

    return new PageMetadata(path, {
        tags,
        aliases,
        links,
        lists: markdownData.lists,
        fields: finalizeInlineFields(fields),
        frontmatter: metadata.frontmatter,
        ctime: DateTime.fromMillis(stat.ctime),
        mtime: DateTime.fromMillis(stat.mtime),
        size: stat.size,
        day: findDate(path, fields),
    });
}
Example #18
Source File: import-manager.ts    From obsidian-dataview with MIT License 5 votes vote down vote up
public async reload<T>(file: TFile): Promise<T> {
        let contents = await this.vault.read(file);
        let metadata = await this.metadataCache.getFileCache(file);
        return runImport(file.path, contents, file.stat, metadata as CachedMetadata) as any as T;
    }
Example #19
Source File: folder-mark.ts    From alx-folder-note with MIT License 5 votes vote down vote up
private _updateIcon(path: string, revert: boolean, item: afItemMark) {
    const api = this.plugin.IconSCAPI;
    if (!api) return;

    let folderNotePath: string | undefined,
      metadata: CachedMetadata | undefined;
    const revertIcon = () => {
      delete item.el.dataset.icon;
      delete item.el.dataset["icon-type"];
      this.foldersWithIcon.delete(path);
      item.el.style.removeProperty("--alx-folder-icon-txt");
      item.el.style.removeProperty("--alx-folder-icon-url");
    };
    if (revert) {
      revertIcon();
    } else if (
      (folderNotePath = this.fncApi.getFolderNotePath(path)?.path) &&
      (metadata = this.plugin.app.metadataCache.getCache(folderNotePath))
    ) {
      let iconId = metadata.frontmatter?.icon,
        icon;
      if (
        iconId &&
        typeof iconId === "string" &&
        (icon = api.getIcon(iconId, true))
      ) {
        this.foldersWithIcon.add(path);
        item.el.dataset.icon = iconId.replace(/^:|:$/g, "");
        if (!api.isEmoji(iconId)) {
          item.el.dataset.iconType = "svg";
          item.el.style.setProperty("--alx-folder-icon-url", `url("${icon}")`);
          item.el.style.setProperty("--alx-folder-icon-txt", '"  "');
        } else {
          item.el.dataset.iconType = "emoji";
          item.el.style.setProperty("--alx-folder-icon-url", '""');
          item.el.style.setProperty("--alx-folder-icon-txt", `"${icon}"`);
        }
      } else if (item.el.dataset.icon) {
        revertIcon();
      }
    }
  }
Example #20
Source File: format.ts    From Obsidian_to_Anki with GNU General Public License v3.0 5 votes vote down vote up
file_cache: CachedMetadata
Example #21
Source File: format.ts    From Obsidian_to_Anki with GNU General Public License v3.0 5 votes vote down vote up
constructor(file_cache: CachedMetadata, vault_name: string) {
		this.vault_name = vault_name
		this.file_cache = file_cache
		this.detectedMedia = new Set()
	}
Example #22
Source File: file.ts    From Obsidian_to_Anki with GNU General Public License v3.0 5 votes vote down vote up
constructor(file_contents: string, path:string, url: string, data: FileData, file_cache: CachedMetadata) {
        super(file_contents, path, url, data, file_cache)
        this.custom_regexps = data.custom_regexps
    }
Example #23
Source File: headingshandler.test.ts    From obsidian-switcher-plus with GNU General Public License v3.0 4 votes vote down vote up
describe('headingsHandler', () => {
  let settings: SwitcherPlusSettings;
  let headingSugg: HeadingSuggestion;
  const mockPrepareQuery = jest.mocked<typeof prepareQuery>(prepareQuery);
  const mockFuzzySearch = jest.mocked<typeof fuzzySearch>(fuzzySearch);

  beforeAll(() => {
    settings = new SwitcherPlusSettings(null);

    jest.spyOn(settings, 'headingsListCommand', 'get').mockReturnValue(headingsTrigger);

    headingSugg = {
      item: makeHeading('foo heading', 1),
      file: new TFile(),
      match: null,
      type: 'heading',
    };
  });

  describe('commandString', () => {
    it('should return headingsListCommand trigger', () => {
      const sut = new HeadingsHandler(mock<App>(), settings);
      expect(sut.commandString).toBe(headingsTrigger);
    });
  });

  describe('validateCommand', () => {
    it('should validate parsed input for headings mode', () => {
      const filterText = 'foo';
      const inputText = `${headingsTrigger}${filterText}`;
      const startIndex = headingsTrigger.length;
      const inputInfo = new InputInfo(inputText);

      const sut = new HeadingsHandler(mock<App>(), settings);
      sut.validateCommand(inputInfo, startIndex, filterText, null, null);
      expect(inputInfo.mode).toBe(Mode.HeadingsList);

      const headingsCmd = inputInfo.parsedCommand();
      expect(headingsCmd.parsedInput).toBe(filterText);
      expect(headingsCmd.isValidated).toBe(true);
    });
  });

  describe('getSuggestions', () => {
    let sut: HeadingsHandler;
    let mockWorkspace: MockProxy<Workspace>;
    let mockVault: MockProxy<Vault>;
    let mockMetadataCache: MockProxy<MetadataCache>;
    let mockViewRegistry: MockProxy<ViewRegistry>;
    let builtInSystemOptionsSpy: jest.SpyInstance;

    beforeAll(() => {
      mockWorkspace = mock<Workspace>();
      mockVault = mock<Vault>();
      mockMetadataCache = mock<MetadataCache>();
      mockViewRegistry = mock<ViewRegistry>();
      mockViewRegistry.isExtensionRegistered.mockReturnValue(true);

      const mockApp = mock<App>({
        workspace: mockWorkspace,
        vault: mockVault,
        metadataCache: mockMetadataCache,
        viewRegistry: mockViewRegistry,
      });

      sut = new HeadingsHandler(mockApp, settings);

      builtInSystemOptionsSpy = jest
        .spyOn(settings, 'builtInSystemOptions', 'get')
        .mockReturnValue({
          showAllFileTypes: true,
          showAttachments: true,
          showExistingOnly: false,
        });
    });

    afterAll(() => {
      builtInSystemOptionsSpy.mockRestore();
    });

    test('with falsy input, it should return an empty array', () => {
      const results = sut.getSuggestions(null);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(0);
    });

    test('without any filter text, it should return most recent opened file suggestions for headings mode', () => {
      const fileData: Record<string, TFile> = {};
      let file = new TFile();
      fileData[file.path] = file;

      file = new TFile();
      fileData[file.path] = file;

      file = new TFile();
      fileData[file.path] = file;

      const fileDataKeys = Object.keys(fileData);
      mockWorkspace.getLastOpenFiles.mockReturnValueOnce(fileDataKeys);
      mockVault.getAbstractFileByPath.mockImplementation(
        (path: string) => fileData[path],
      );
      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        return f === file ? {} : getCachedMetadata();
      });

      const inputInfo = new InputInfo(headingsTrigger);
      const results = sut.getSuggestions(inputInfo);

      expect(results).toHaveLength(fileDataKeys.length);

      const expectedFiles = new Set(Object.values(fileData));
      const headingSuggestions = results.filter((sugg) =>
        isHeadingSuggestion(sugg),
      ) as HeadingSuggestion[];

      expect(headingSuggestions).toHaveLength(2);
      expect(headingSuggestions.every((sugg) => expectedFiles.has(sugg.file))).toBe(true);

      const fileSuggestions = results.filter((sugg) =>
        isFileSuggestion(sugg),
      ) as FileSuggestion[];

      expect(fileSuggestions).toHaveLength(1);
      expect(fileSuggestions.every((sugg) => expectedFiles.has(sugg.file))).toBe(true);

      expect(mockWorkspace.getLastOpenFiles).toHaveBeenCalled();
      expect(mockVault.getAbstractFileByPath).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockWorkspace.getLastOpenFiles.mockReset();
      mockVault.getAbstractFileByPath.mockReset();
      mockMetadataCache.getFileCache.mockReset();
    });

    test('with filter search term, it should return matching suggestions for all headings', () => {
      const expected = new TFile();
      const h1 = makeHeading('foo heading H1', 1, makeLoc(1));
      const h2 = makeHeading('foo heading H2', 2, makeLoc(2));
      const filterText = 'foo';

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));

      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        return f === expected ? { headings: [h1, h2] } : getCachedMetadata();
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(2);

      expect(results.every((r) => isHeadingSuggestion(r))).toBe(true);

      expect(
        results.every((r: HeadingSuggestion) => r.item === h1 || r.item === h2),
      ).toBe(true);

      const result = results[0] as HeadingSuggestion;
      expect(result.file).toBe(expected);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
    });

    test('with filter search term, and searchAllHeadings set to false, it should return only matching suggestions using first H1 in file', () => {
      const expected = new TFile();
      const expectedHeading = makeHeading('foo heading H1', 1, makeLoc(1));
      const heading2 = makeHeading('foo heading H1', 1, makeLoc(2));
      const filterText = 'foo';

      const searchAllHeadingsSpy = jest
        .spyOn(settings, 'searchAllHeadings', 'get')
        .mockReturnValue(false);

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));

      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        return f === expected
          ? { headings: [expectedHeading, heading2] }
          : getCachedMetadata();
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(1);

      let result = results[0];
      expect(isHeadingSuggestion(result)).toBe(true);

      result = result as HeadingSuggestion;
      expect(result.file).toBe(expected);
      expect(result.item).toBe(expectedHeading);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
      searchAllHeadingsSpy.mockRestore();
    });

    test("with filter search term, it should return matching suggestions using file name (leaf segment) when H1 doesn't exist", () => {
      const filterText = 'foo';
      const expected = new TFile();
      expected.path = 'path/to/bar/foo filename.md'; // only path matters for this test

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));

      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        // don't return any heading metadata for expected
        return f === expected ? {} : getCachedMetadata();
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(1);

      const result = results[0];
      expect(isFileSuggestion(result)).toBe(true);
      expect((result as FileSuggestion).file).toBe(expected);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
    });

    test("with filter search term, it should fallback to match against file path when H1 doesn't exist and there's no match against the filename (leaf segment)", () => {
      const filterText = 'foo';
      const expected = new TFile();
      expected.path = 'foo/path/to/filename.md'; // only path matters for this test

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));
      mockMetadataCache.getFileCache.mockReturnValue({});

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(1);

      const result = results[0];
      expect(isFileSuggestion(result)).toBe(true);
      expect((result as FileSuggestion).file).toBe(expected);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
    });

    test('with filter search term and shouldShowAlias set to true, it should match against aliases', () => {
      const expected = new TFile();
      const filterText = 'foo';

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));
      settings.shouldShowAlias = true;

      const fm: CachedMetadata = {
        frontmatter: {
          aliases: ['bar', 'foo'],
          position: null,
        },
      };

      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        return f === expected ? fm : getCachedMetadata();
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(1);

      const result = results[0];
      expect(isAliasSuggestion(result)).toBe(true);
      expect((result as AliasSuggestion).file).toBe(expected);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      settings.shouldShowAlias = false;
      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
    });

    test('with filter search term and showExistingOnly set to false, it should match against unresolved linktext', () => {
      const expected = new TFile();
      const filterText = 'foo';

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));

      mockMetadataCache.unresolvedLinks[expected.path] = {
        'foo link noexist': 1,
        'another link': 1,
      };

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(1);

      const result = results[0];
      expect(isUnresolvedSuggestion(result)).toBe(true);
      expect((result as UnresolvedSuggestion).linktext).toBe('foo link noexist');

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
      mockMetadataCache.unresolvedLinks = {};
    });

    test('with filter search term and strictHeadingsOnly enabled, it should not match against file name, or path when there is no H1', () => {
      const filterText = 'foo';
      const expected = new TFile();
      expected.path = 'foo/path/to/filename.md'; // only path matters for this test

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected));
      mockMetadataCache.getFileCache.mockReturnValue({});

      const strictHeadingsOnlySpy = jest
        .spyOn(settings, 'strictHeadingsOnly', 'get')
        .mockReturnValue(true);

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(0);

      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();
      expect(strictHeadingsOnlySpy).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockPrepareQuery.mockReset();
      strictHeadingsOnlySpy.mockRestore();
    });

    it('should not return suggestions from excluded folders', () => {
      const filterText = 'foo';
      const excludedFolderName = 'ignored';
      const h1 = makeHeading('foo heading H1', 1, makeLoc(1));
      const expected = new TFile();
      expected.path = 'foo/path/to/foo filename.md';

      mockPrepareQuery.mockReturnValue(makePreparedQuery(filterText));
      mockVault.getRoot.mockReturnValueOnce(makeFileTree(expected, excludedFolderName));

      mockMetadataCache.getFileCache.mockImplementation((f: TFile) => {
        return f === expected ? { headings: [h1] } : {};
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.startsWith(filterText) ? match : null;
      });

      const excludeFoldersSpy = jest
        .spyOn(settings, 'excludeFolders', 'get')
        .mockReturnValue([excludedFolderName]);

      const inputInfo = new InputInfo(`${headingsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, null);

      const results = sut.getSuggestions(inputInfo);
      expect(results).toHaveLength(0);

      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockVault.getRoot).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalled();
      expect(builtInSystemOptionsSpy).toHaveBeenCalled();
      expect(mockViewRegistry.isExtensionRegistered).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
      mockPrepareQuery.mockReset();
      excludeFoldersSpy.mockRestore();
    });
  });

  describe('addSuggestionsFromFile', () => {
    let sut: HeadingsHandler;
    let mockWorkspace: MockProxy<Workspace>;
    // let mockVault: MockProxy<Vault>;
    let mockMetadataCache: MockProxy<MetadataCache>;
    let mockViewRegistry: MockProxy<ViewRegistry>;
    let builtInSystemOptionsSpy: jest.SpyInstance;

    beforeAll(() => {
      mockWorkspace = mock<Workspace>();
      // mockVault = mock<Vault>();
      mockMetadataCache = mock<MetadataCache>();
      mockViewRegistry = mock<ViewRegistry>();
      mockViewRegistry.isExtensionRegistered.mockReturnValue(true);

      const mockApp = mock<App>({
        workspace: mockWorkspace,
        // vault: mockVault,
        metadataCache: mockMetadataCache,
        viewRegistry: mockViewRegistry,
      });

      builtInSystemOptionsSpy = jest
        .spyOn(settings, 'builtInSystemOptions', 'get')
        .mockReturnValue({
          showAllFileTypes: true,
          showAttachments: true,
          showExistingOnly: false,
        });

      sut = new HeadingsHandler(mockApp, settings);
    });

    afterAll(() => {
      builtInSystemOptionsSpy.mockRestore();
    });

    test('with filter search term, it should return matching suggestions using file name (leaf segment) when there is no H1 match', () => {
      const filterText = 'foo';
      const filename = `${filterText} filename`;
      const path = `path/${filterText}/bar/${filename}`; // only path matters for this test
      const results: Array<FileSuggestion> = [];
      const expectedMatch = makeFuzzyMatch();

      const expectedFile = new TFile();
      expectedFile.path = path;

      mockMetadataCache.getFileCache.calledWith(expectedFile).mockReturnValue({
        headings: [makeHeading("words that don't match", 1)],
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        return text === filename ? expectedMatch : null;
      });

      sut.addSuggestionsFromFile(results, expectedFile, makePreparedQuery(filterText));

      const result = results[0];
      expect(results).toHaveLength(1);
      expect(isFileSuggestion(result)).toBe(true);
      expect(result.file).toBe(expectedFile);
      expect(result.match).toBe(expectedMatch);
      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(expectedFile);

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
    });

    test('with filter search term, it should fallback match against file path when there is no H1 match and no match against the filename (leaf segment)', () => {
      const filterText = 'foo';
      const path = `path/${filterText}/bar/filename`; // only path matters for this test
      const results: Array<FileSuggestion> = [];
      const expectedMatch = makeFuzzyMatch();

      const expectedFile = new TFile();
      expectedFile.path = path;

      mockMetadataCache.getFileCache.calledWith(expectedFile).mockReturnValue({
        headings: [makeHeading("words that don't match", 1)],
      });

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        return text === path ? expectedMatch : null;
      });

      sut.addSuggestionsFromFile(results, expectedFile, makePreparedQuery(filterText));

      const result = results[0];
      expect(results).toHaveLength(1);
      expect(isFileSuggestion(result)).toBe(true);
      expect(result.file).toBe(expectedFile);
      expect(result.match).toBe(expectedMatch);
      expect(mockFuzzySearch).toHaveBeenCalled();
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(expectedFile);

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
    });
  });

  describe('renderSuggestion', () => {
    let sut: HeadingsHandler;
    let mockParentEl: MockProxy<HTMLElement>;

    beforeAll(() => {
      sut = new HeadingsHandler(mock<App>(), settings);
      mockParentEl = mock<HTMLElement>();
    });

    it('should not throw an error with a null suggestion', () => {
      expect(() => sut.renderSuggestion(null, null)).not.toThrow();
    });

    it('should render a span with the heading level indicator', () => {
      sut.renderSuggestion(headingSugg, mockParentEl);

      expect(mockParentEl.createSpan).toHaveBeenCalledWith(
        expect.objectContaining({
          cls: ['suggestion-flair', 'qsp-headings-indicator'],
          text: HeadingIndicators[headingSugg.item.level],
          prepend: true,
        }),
      );
    });

    test('with HeadingCache, it should render a suggestion with match offsets', () => {
      const mockRenderResults = jest.mocked<typeof renderResults>(renderResults);

      sut.renderSuggestion(headingSugg, mockParentEl);

      expect(mockRenderResults).toHaveBeenCalledWith(
        mockParentEl,
        headingSugg.item.heading,
        headingSugg.match,
      );
    });

    it('should render a div element with the text of the suggestion file path', () => {
      sut.renderSuggestion(headingSugg, mockParentEl);

      expect(mockParentEl.createDiv).toHaveBeenCalledWith(
        expect.objectContaining({
          cls: 'suggestion-note',
          text: stripMDExtensionFromPath(headingSugg.file),
        }),
      );
    });
  });

  describe('onChooseSuggestion', () => {
    const mockKeymap = jest.mocked<typeof Keymap>(Keymap);
    let sut: HeadingsHandler;
    let mockWorkspace: MockProxy<Workspace>;

    beforeAll(() => {
      mockWorkspace = mock<Workspace>();
      const mockApp = mock<App>({
        workspace: mockWorkspace,
      });

      const fileContainerLeaf = makeLeaf();
      fileContainerLeaf.openFile.mockResolvedValueOnce();
      mockWorkspace.getLeaf.mockReturnValueOnce(fileContainerLeaf);

      sut = new HeadingsHandler(mockApp, settings);
    });

    it('should not throw an error with a null suggestion', () => {
      expect(() => sut.onChooseSuggestion(null, null)).not.toThrow();
    });

    it('should open the file associated with the suggestion', () => {
      const isModDown = false;
      const navigateToLeafOrOpenFileSpy = jest.spyOn(
        Handler.prototype,
        'navigateToLeafOrOpenFile',
      );

      mockKeymap.isModEvent.mockReturnValueOnce(isModDown);

      sut.onChooseSuggestion(headingSugg, null);

      expect(mockKeymap.isModEvent).toHaveBeenCalled();
      expect(navigateToLeafOrOpenFileSpy).toHaveBeenCalledWith(
        isModDown,
        headingSugg.file,
        expect.any(String),
        expect.anything(),
      );

      navigateToLeafOrOpenFileSpy.mockRestore();
    });
  });
});
Example #24
Source File: markdown-file.ts    From obsidian-dataview with MIT License 4 votes vote down vote up
/**
 * Parse list items from the page + metadata. This requires some additional parsing above whatever Obsidian provides,
 * since Obsidian only gives line numbers.
 */
export function parseLists(
    path: string,
    content: string[],
    metadata: CachedMetadata
): [ListItem[], Map<string, Literal[]>] {
    let cache: Record<number, ListItem> = {};

    // Place all of the values in the cache before resolving children & metadata relationships.
    for (let rawElement of metadata.listItems || []) {
        // Match on the first line to get the symbol and first line of text.
        let rawMatch = LIST_ITEM_REGEX.exec(content[rawElement.position.start.line]);
        if (!rawMatch) continue;

        // And then strip unnecessary spacing from the remaining lines.
        let textParts = [rawMatch[3]]
            .concat(content.slice(rawElement.position.start.line + 1, rawElement.position.end.line + 1))
            .map(t => t.trim());
        let textWithNewline = textParts.join("\n");
        let textNoNewline = textParts.join(" ");

        // Find the list that we are a part of by line.
        let containingListId = (metadata.sections || []).findIndex(
            s =>
                s.type == "list" &&
                s.position.start.line <= rawElement.position.start.line &&
                s.position.end.line >= rawElement.position.start.line
        );

        // Find the section we belong to as well.
        let sectionName = findPreviousHeader(rawElement.position.start.line, metadata.headings || []);
        let sectionLink = sectionName === undefined ? Link.file(path) : Link.header(path, sectionName);
        let closestLink = rawElement.id === undefined ? sectionLink : Link.block(path, rawElement.id);

        // Construct universal information about this element (before tasks).
        let item = new ListItem({
            symbol: rawMatch[1],
            link: closestLink,
            section: sectionLink,
            text: textWithNewline,
            tags: common.extractTags(textNoNewline),
            line: rawElement.position.start.line,
            lineCount: rawElement.position.end.line - rawElement.position.start.line + 1,
            list: containingListId == -1 ? -1 : (metadata.sections || [])[containingListId].position.start.line,
            position: rawElement.position,
            children: [],
            blockId: rawElement.id,
        });

        if (rawElement.parent >= 0 && rawElement.parent != item.line) item.parent = rawElement.parent;

        // Set up the basic task information for now, though we have to recompute `fullyComputed` later.
        if (rawElement.task) {
            item.task = {
                status: rawElement.task,
                checked: rawElement.task != "" && rawElement.task != " ",
                completed: rawElement.task == "X" || rawElement.task == "x",
                fullyCompleted: rawElement.task == "X" || rawElement.task == "x",
            };
        }

        // Extract inline fields; extract full-line fields only if we are NOT a task.
        item.fields = new Map<string, Literal[]>();
        for (let element of extractInlineFields(textNoNewline, true)) addRawInlineField(element, item.fields);

        if (!rawElement.task && item.fields.size == 0) {
            let fullLine = extractFullLineField(textNoNewline);
            if (fullLine) addRawInlineField(fullLine, item.fields);
        }

        cache[item.line] = item;
    }

    // Tree updating passes. Update child lists. Propogate metadata up to parent tasks. Update task `fullyCompleted`.
    let literals: Map<string, Literal[]> = new Map();
    for (let listItem of Object.values(cache)) {
        // Pass 1: Update child lists.
        if (listItem.parent !== undefined && listItem.parent in cache) {
            let parent = cache[listItem.parent!!];
            parent.children.push(listItem.line);
        }

        // Pass 2: Propogate metadata up to the parent task or root element.
        if (!listItem.task) {
            mergeFieldGroups(literals, listItem.fields);

            // TODO (blacksmithgu): The below code properly propogates metadata up to the nearest task, which is the
            // more intuitive behavior. For now, though, we will keep the existing logic.
            /*
            let root: ListItem | undefined = listItem;
            while (!!root && !root.task) root = cache[root.parent ?? -1];

            // If the root is null, append this metadata to the root; otherwise, append to the task.
            mergeFieldGroups(root === undefined || root == null ? literals : root.fields, listItem.fields);
            */
        }

        // Pass 3: Propogate `fullyCompleted` up the task tree. This is a little less efficient than just doing a simple
        // DFS using the children IDs, but it's probably fine.
        if (listItem.task) {
            let curr: ListItem | undefined = listItem;
            while (!!curr) {
                if (curr.task) curr.task.fullyCompleted = curr.task.fullyCompleted && listItem.task.completed;
                curr = cache[curr.parent ?? -1];
            }
        }
    }

    return [Object.values(cache), literals];
}
Example #25
Source File: watcher.worker.ts    From obsidian-fantasy-calendar with MIT License 4 votes vote down vote up
parseFileForEvents(
        data: string,
        cache: CachedMetadata,
        allTags: string[],
        file: { path: string; basename: string }
    ) {
        const events = [];
        const { frontmatter } = cache ?? {};

        const { calendar, fcCategory, eventDisplayName } =
            this.getDataFromFrontmatter(frontmatter);

        if (!calendar) {
            if (this.debug) {
                console.info(
                    `Could not find calendar associated with file ${file.basename}`
                );
            }
            this.removeEventsFromFile(file.path);
            return;
        }

        let timelineEvents: Event[] = [];
        if (
            calendar.supportTimelines &&
            allTags &&
            allTags
                .map((tag) => tag.replace(/#/, ""))
                .includes(calendar.timelineTag.replace(/#/, ""))
        ) {
            timelineEvents = this.parseTimelineEvents(
                calendar,
                data,
                file,
                fcCategory
            );
            events.push(...timelineEvents);
        }

        const frontmatterEvents = this.parseFrontmatterEvents(
            calendar,
            fcCategory,
            frontmatter,
            file,
            eventDisplayName
        );

        events.push(...frontmatterEvents);
        if (!events || !events.length) {
            this.removeEventsFromFile(file.path);
            return;
        }

        let added = 0;
        for (const event of events) {
            //TODO: Remove event existing check. Old events matching the file should just be removed.
            const existing = calendar.events.find(
                (exist) =>
                    exist.note == file.path &&
                    (!event.timestamp || exist.timestamp == event.timestamp)
            );
            if (
                existing?.date.day == event.date.day &&
                existing?.date.month == event.date.month &&
                existing?.date.year == event.date.year &&
                existing?.end?.day == event.end?.day &&
                existing?.end?.month == event.end?.month &&
                existing?.end?.year == event.end?.year &&
                existing?.category == event.category &&
                existing?.name == event.name &&
                ((!event.timestamp && !existing?.timestamp) ||
                    existing?.timestamp == event.timestamp)
            ) {
                continue;
            }

            ctx.postMessage<UpdateEventMessage>({
                type: "update",
                id: calendar.id,
                index: existing
                    ? calendar.events.findIndex((e) => e.id == existing?.id)
                    : -1,
                event,
                original: existing
            });
            added++;
        }
        if (this.debug && events.length > 0) {
            console.info(
                `${added}/${events.length} (${frontmatterEvents.length} from frontmatter, ${timelineEvents.length} from timelines) event operations completed on ${calendar.name} for ${file.basename}`
            );
        }
    }
Example #26
Source File: collecting.ts    From obsidian-tracker with MIT License 4 votes vote down vote up
// In form 'key: value', target used to identify 'frontmatter key'
export function collectDataFromFrontmatterKey(
    fileCache: CachedMetadata,
    query: Query,
    renderInfo: RenderInfo,
    dataMap: DataMap,
    xValueMap: XValueMap
): boolean {
    // console.log("collectDataFromFrontmatterKey");

    let frontMatter = fileCache.frontmatter;
    if (frontMatter) {
        // console.log(frontMatter);
        // console.log(query.getTarget());
        let deepValue = helper.deepValue(frontMatter, query.getTarget());
        // console.log(deepValue);
        if (deepValue) {
            let retParse = helper.parseFloatFromAny(
                deepValue,
                renderInfo.textValueMap
            );
            // console.log(retParse);
            if (retParse.value !== null) {
                if (retParse.type === ValueType.Time) {
                    query.valueType = ValueType.Time;
                }
                query.addNumTargets();
                let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]);
                addToDataMap(dataMap, xValue, query, retParse.value);
                return true;
            }
        } else if (
            query.getParentTarget() &&
            helper.deepValue(frontMatter, query.getParentTarget())
        ) {
            // console.log("multiple values");
            // console.log(query.getTarget());
            // console.log(query.getParentTarget());
            // console.log(query.getSubId());
            // console.log(
            //     frontMatter[query.getParentTarget()]
            // );
            let toParse = helper.deepValue(
                frontMatter,
                query.getParentTarget()
            );
            let splitted = null;
            if (Array.isArray(toParse)) {
                splitted = toParse.map((p) => {
                    return p.toString();
                });
            } else if (typeof toParse === "string") {
                splitted = toParse.split(query.getSeparator());
            }
            // console.log(splitted);
            if (
                splitted &&
                splitted.length > query.getAccessor() &&
                query.getAccessor() >= 0
            ) {
                // TODO: it's not efficent to retrieve one value at a time, enhance this
                let splittedPart = splitted[query.getAccessor()].trim();
                let retParse = helper.parseFloatFromAny(
                    splittedPart,
                    renderInfo.textValueMap
                );
                if (retParse.value !== null) {
                    if (retParse.type === ValueType.Time) {
                        query.valueType = ValueType.Time;
                    }
                    query.addNumTargets();
                    let xValue = xValueMap.get(
                        renderInfo.xDataset[query.getId()]
                    );
                    addToDataMap(dataMap, xValue, query, retParse.value);
                    return true;
                }
            }
        }
    }

    return false;
}
Example #27
Source File: main.ts    From obsidian-tracker with MIT License 4 votes vote down vote up
async postprocessor(
        source: string,
        el: HTMLElement,
        ctx: MarkdownPostProcessorContext
    ) {
        // console.log("postprocess");
        const canvas = document.createElement("div");

        let yamlText = source.trim();

        // Replace all tabs by spaces
        let tabSize = this.app.vault.getConfig("tabSize");
        let spaces = Array(tabSize).fill(" ").join("");
        yamlText = yamlText.replace(/\t/gm, spaces);

        // Get render info
        let retRenderInfo = getRenderInfoFromYaml(yamlText, this);
        if (typeof retRenderInfo === "string") {
            return this.renderErrorMessage(retRenderInfo, canvas, el);
        }
        let renderInfo = retRenderInfo as RenderInfo;
        // console.log(renderInfo);

        // Get files
        let files: TFile[] = [];
        try {
            await this.getFiles(files, renderInfo);
        } catch (e) {
            return this.renderErrorMessage(e.message, canvas, el);
        }
        if (files.length === 0) {
            return this.renderErrorMessage(
                "No markdown files found in folder",
                canvas,
                el
            );
        }
        // console.log(files);

        // let dailyNotesSettings = getDailyNoteSettings();
        // console.log(dailyNotesSettings);
        // I always got YYYY-MM-DD from dailyNotesSettings.format
        // Use own settings panel for now

        // Collecting data to dataMap first
        let dataMap: DataMap = new Map(); // {strDate: [query: value, ...]}
        let processInfo = new CollectingProcessInfo();
        processInfo.fileTotal = files.length;

        // Collect data from files, each file has one data point for each query
        const loopFilePromises = files.map(async (file) => {
            // console.log(file.basename);
            // Get fileCache and content
            let fileCache: CachedMetadata = null;
            let needFileCache = renderInfo.queries.some((q) => {
                let type = q.getType();
                let target = q.getTarget();
                if (
                    type === SearchType.Frontmatter ||
                    type === SearchType.Tag ||
                    type === SearchType.Wiki ||
                    type === SearchType.WikiLink ||
                    type === SearchType.WikiDisplay
                ) {
                    return true;
                }
                return false;
            });
            if (needFileCache) {
                fileCache = this.app.metadataCache.getFileCache(file);
            }

            let content: string = null;
            let needContent = renderInfo.queries.some((q) => {
                let type = q.getType();
                let target = q.getTarget();
                if (
                    type === SearchType.Tag ||
                    type === SearchType.Text ||
                    type === SearchType.dvField ||
                    type === SearchType.Task ||
                    type === SearchType.TaskDone ||
                    type === SearchType.TaskNotDone
                ) {
                    return true;
                } else if (type === SearchType.FileMeta) {
                    if (
                        target === "numWords" ||
                        target === "numChars" ||
                        target === "numSentences"
                    ) {
                        return true;
                    }
                }
                return false;
            });
            if (needContent) {
                content = await this.app.vault.adapter.read(file.path);
            }

            // Get xValue and add it into xValueMap for later use
            let xValueMap: XValueMap = new Map(); // queryId: xValue for this file
            let skipThisFile = false;
            // console.log(renderInfo.xDataset);
            for (let xDatasetId of renderInfo.xDataset) {
                // console.log(`xDatasetId: ${xDatasetId}`);
                if (!xValueMap.has(xDatasetId)) {
                    let xDate = window.moment("");
                    if (xDatasetId === -1) {
                        // Default using date in filename as xValue
                        xDate = collecting.getDateFromFilename(
                            file,
                            renderInfo
                        );
                        // console.log(xDate);
                    } else {
                        let xDatasetQuery = renderInfo.queries[xDatasetId];
                        // console.log(xDatasetQuery);
                        switch (xDatasetQuery.getType()) {
                            case SearchType.Frontmatter:
                                xDate = collecting.getDateFromFrontmatter(
                                    fileCache,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                            case SearchType.Tag:
                                xDate = collecting.getDateFromTag(
                                    content,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                            case SearchType.Text:
                                xDate = collecting.getDateFromText(
                                    content,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                            case SearchType.dvField:
                                xDate = collecting.getDateFromDvField(
                                    content,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                            case SearchType.FileMeta:
                                xDate = collecting.getDateFromFileMeta(
                                    file,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                            case SearchType.Task:
                            case SearchType.TaskDone:
                            case SearchType.TaskNotDone:
                                xDate = collecting.getDateFromTask(
                                    content,
                                    xDatasetQuery,
                                    renderInfo
                                );
                                break;
                        }
                    }

                    if (!xDate.isValid()) {
                        // console.log("Invalid xDate");
                        skipThisFile = true;
                        processInfo.fileNotInFormat++;
                    } else {
                        // console.log("file " + file.basename + " accepted");
                        if (renderInfo.startDate !== null) {
                            if (xDate < renderInfo.startDate) {
                                skipThisFile = true;
                                processInfo.fileOutOfDateRange++;
                            }
                        }
                        if (renderInfo.endDate !== null) {
                            if (xDate > renderInfo.endDate) {
                                skipThisFile = true;
                                processInfo.fileOutOfDateRange++;
                            }
                        }
                    }

                    if (!skipThisFile) {
                        processInfo.gotAnyValidXValue ||= true;
                        xValueMap.set(
                            xDatasetId,
                            helper.dateToStr(xDate, renderInfo.dateFormat)
                        );
                        processInfo.fileAvailable++;

                        // Get min/max date
                        if (processInfo.fileAvailable == 1) {
                            processInfo.minDate = xDate.clone();
                            processInfo.maxDate = xDate.clone();
                        } else {
                            if (xDate < processInfo.minDate) {
                                processInfo.minDate = xDate.clone();
                            }
                            if (xDate > processInfo.maxDate) {
                                processInfo.maxDate = xDate.clone();
                            }
                        }
                    }
                }
            }
            if (skipThisFile) return;
            // console.log(xValueMap);
            // console.log(`minDate: ${minDate}`);
            // console.log(`maxDate: ${maxDate}`);

            // Loop over queries
            let yDatasetQueries = renderInfo.queries.filter((q) => {
                return q.getType() !== SearchType.Table && !q.usedAsXDataset;
            });
            // console.log(yDatasetQueries);

            const loopQueryPromises = yDatasetQueries.map(async (query) => {
                // Get xValue from file if xDataset assigned
                // if (renderInfo.xDataset !== null)
                // let xDatasetId = renderInfo.xDataset;
                // console.log(query);

                // console.log("Search frontmatter tags");
                if (fileCache && query.getType() === SearchType.Tag) {
                    // Add frontmatter tags, allow simple tag only
                    let gotAnyValue = collecting.collectDataFromFrontmatterTag(
                        fileCache,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // Search frontmatter tags

                // console.log("Search frontmatter keys");
                if (
                    fileCache &&
                    query.getType() === SearchType.Frontmatter &&
                    query.getTarget() !== "tags"
                ) {
                    let gotAnyValue = collecting.collectDataFromFrontmatterKey(
                        fileCache,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // console.log("Search frontmatter keys");

                // console.log("Search wiki links");
                if (
                    fileCache &&
                    (query.getType() === SearchType.Wiki ||
                        query.getType() === SearchType.WikiLink ||
                        query.getType() === SearchType.WikiDisplay)
                ) {
                    let gotAnyValue = collecting.collectDataFromWiki(
                        fileCache,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                }

                // console.log("Search inline tags");
                if (content && query.getType() === SearchType.Tag) {
                    let gotAnyValue = collecting.collectDataFromInlineTag(
                        content,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // Search inline tags

                // console.log("Search Text");
                if (content && query.getType() === SearchType.Text) {
                    let gotAnyValue = collecting.collectDataFromText(
                        content,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // Search text

                // console.log("Search FileMeta");
                if (query.getType() === SearchType.FileMeta) {
                    let gotAnyValue = collecting.collectDataFromFileMeta(
                        file,
                        content,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // Search FileMeta

                // console.log("Search dvField");
                if (content && query.getType() === SearchType.dvField) {
                    let gotAnyValue = collecting.collectDataFromDvField(
                        content,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // search dvField

                // console.log("Search Task");
                if (
                    content &&
                    (query.getType() === SearchType.Task ||
                        query.getType() === SearchType.TaskDone ||
                        query.getType() === SearchType.TaskNotDone)
                ) {
                    let gotAnyValue = collecting.collectDataFromTask(
                        content,
                        query,
                        renderInfo,
                        dataMap,
                        xValueMap
                    );
                    processInfo.gotAnyValidYValue ||= gotAnyValue;
                } // search Task
            });
            await Promise.all(loopQueryPromises);
        });
        await Promise.all(loopFilePromises);
        // console.log(dataMap);

        // Collect data from a file, one file contains full dataset
        await this.collectDataFromTable(dataMap, renderInfo, processInfo);
        if (processInfo.errorMessage) {
            return this.renderErrorMessage(
                processInfo.errorMessage,
                canvas,
                el
            );
        }
        // console.log(minDate);
        // console.log(maxDate);
        // console.log(dataMap);

        // Check date range
        // minDate and maxDate are collected without knowing startDate and endDate
        // console.log(`fileTotal: ${processInfo.fileTotal}`);
        // console.log(`fileAvailable: ${processInfo.fileAvailable}`);
        // console.log(`fileNotInFormat: ${processInfo.fileNotInFormat}`);
        // console.log(`fileOutOfDateRange: ${processInfo.fileOutOfDateRange}`);
        let dateErrorMessage = "";
        if (
            !processInfo.minDate.isValid() ||
            !processInfo.maxDate.isValid() ||
            processInfo.fileAvailable === 0 ||
            !processInfo.gotAnyValidXValue
        ) {
            dateErrorMessage = `No valid date as X value found in notes`;
            if (processInfo.fileOutOfDateRange > 0) {
                dateErrorMessage += `\n${processInfo.fileOutOfDateRange} files are out of the date range.`;
            }
            if (processInfo.fileNotInFormat) {
                dateErrorMessage += `\n${processInfo.fileNotInFormat} files are not in the right format.`;
            }
        }
        if (renderInfo.startDate === null && renderInfo.endDate === null) {
            // No date arguments
            renderInfo.startDate = processInfo.minDate.clone();
            renderInfo.endDate = processInfo.maxDate.clone();
        } else if (
            renderInfo.startDate !== null &&
            renderInfo.endDate === null
        ) {
            if (renderInfo.startDate < processInfo.maxDate) {
                renderInfo.endDate = processInfo.maxDate.clone();
            } else {
                dateErrorMessage = "Invalid date range";
            }
        } else if (
            renderInfo.endDate !== null &&
            renderInfo.startDate === null
        ) {
            if (renderInfo.endDate > processInfo.minDate) {
                renderInfo.startDate = processInfo.minDate.clone();
            } else {
                dateErrorMessage = "Invalid date range";
            }
        } else {
            // startDate and endDate are valid
            if (
                (renderInfo.startDate < processInfo.minDate &&
                    renderInfo.endDate < processInfo.minDate) ||
                (renderInfo.startDate > processInfo.maxDate &&
                    renderInfo.endDate > processInfo.maxDate)
            ) {
                dateErrorMessage = "Invalid date range";
            }
        }
        if (dateErrorMessage) {
            return this.renderErrorMessage(dateErrorMessage, canvas, el);
        }
        // console.log(renderInfo.startDate);
        // console.log(renderInfo.endDate);

        if (!processInfo.gotAnyValidYValue) {
            return this.renderErrorMessage(
                "No valid Y value found in notes",
                canvas,
                el
            );
        }

        // Reshape data for rendering
        let datasets = new Datasets(renderInfo.startDate, renderInfo.endDate);
        for (let query of renderInfo.queries) {
            // We still create a dataset for xDataset,
            // to keep the sequence and order of targets
            let dataset = datasets.createDataset(query, renderInfo);
            // Add number of targets to the dataset
            // Number of targets has been accumulated while collecting data
            dataset.addNumTargets(query.getNumTargets());
            for (
                let curDate = renderInfo.startDate.clone();
                curDate <= renderInfo.endDate;
                curDate.add(1, "days")
            ) {
                // console.log(curDate);

                // dataMap --> {date: [query: value, ...]}
                if (
                    dataMap.has(
                        helper.dateToStr(curDate, renderInfo.dateFormat)
                    )
                ) {
                    let queryValuePairs = dataMap
                        .get(helper.dateToStr(curDate, renderInfo.dateFormat))
                        .filter(function (pair) {
                            return pair.query.equalTo(query);
                        });
                    if (queryValuePairs.length > 0) {
                        // Merge values of the same day same query
                        let value = null;
                        for (
                            let indPair = 0;
                            indPair < queryValuePairs.length;
                            indPair++
                        ) {
                            let collected = queryValuePairs[indPair].value;
                            if (
                                Number.isNumber(collected) &&
                                !Number.isNaN(collected)
                            ) {
                                if (value === null) {
                                    value = collected;
                                } else {
                                    value += collected;
                                }
                            }
                        }
                        // console.log(hasValue);
                        // console.log(value);
                        if (value !== null) {
                            dataset.setValue(curDate, value);
                        }
                    }
                }
            }
        }
        renderInfo.datasets = datasets;
        // console.log(renderInfo.datasets);

        let retRender = rendering.render(canvas, renderInfo);
        if (typeof retRender === "string") {
            return this.renderErrorMessage(retRender, canvas, el);
        }

        el.appendChild(canvas);
    }