| 671 | } |
| 672 | |
| 673 | private async updateMultipleInFile(properties: Property[], file: TFile): Promise<void> { |
| 674 | // Refuse a batch with ANY reserved YAML key BEFORE the write starts. This |
| 675 | // method splices body tags before writing frontmatter, so validating up |
| 676 | // front keeps the batch atomic: a reserved key never lets the tag/text |
| 677 | // edits land while the frontmatter write is the part that fails. A nested |
| 678 | // property carries its key in `path`, so every string segment is checked - |
| 679 | // not just `prop.key` - otherwise `setYamlPath` would only throw later, |
| 680 | // inside `processFrontMatter`, after the tag splice already wrote. |
| 681 | for (const prop of properties) { |
| 682 | if (prop.type !== MetaType.YAML) continue; |
| 683 | if (prop.path && prop.path.length > 0) { |
| 684 | for (const segment of prop.path) { |
| 685 | if (typeof segment === "string") this.assertWritableKey(segment); |
| 686 | } |
| 687 | } else { |
| 688 | this.assertWritableKey(prop.key); |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | await this.enqueueFileWrite(file, async () => { |
| 693 | const yamlProperties = properties.filter(prop => prop.type === MetaType.YAML && !prop.path); |
| 694 | const yamlPathProperties = properties.filter(prop => prop.type === MetaType.YAML && prop.path); |
| 695 | const tagProperties = properties.filter(prop => prop.type === MetaType.Tag); |
| 696 | const textProperties = properties.filter(prop => prop.type === MetaType.Dataview && prop.key); |
| 697 | |
| 698 | // Splice body tags FIRST, while their parsed offsets still match the |
| 699 | // file. A later processFrontMatter write only touches the frontmatter, |
| 700 | // so it preserves these body edits; doing it the other way round would |
| 701 | // shift every body offset and stale the tag spans. Highest-offset-first |
| 702 | // keeps earlier spans valid as later ones change length; a stale span |
| 703 | // is skipped, never forced, so a batch never corrupts prose. |
| 704 | if (tagProperties.length > 0) { |
| 705 | let content = await this.app.vault.read(file); |
| 706 | for (const prop of [...tagProperties].sort((a, b) => (b.position?.start ?? 0) - (a.position?.start ?? 0))) { |
| 707 | const token = normalizeTagToken(String(prop.content)); |
| 708 | if (!isValidTagToken(token)) { |
| 709 | log.logMessage(`MetaEdit skipped tag '${prop.key}': '${String(prop.content)}' is not a valid tag.`); |
| 710 | continue; |
| 711 | } |
| 712 | const updated = spliceTag(content, prop.position, prop.key, token); |
| 713 | if (updated === null) { |
| 714 | log.logMessage(`MetaEdit skipped tag '${prop.key}': its position no longer matches the note.`); |
| 715 | continue; |
| 716 | } |
| 717 | content = updated; |
| 718 | } |
| 719 | await this.app.vault.modify(file, content); |
| 720 | } |
| 721 | |
| 722 | if (yamlProperties.length > 0 || yamlPathProperties.length > 0) { |
| 723 | await this.processFrontMatter(file, (frontmatter) => { |
| 724 | for (const prop of yamlProperties) { |
| 725 | frontmatter[prop.key] = prop.content; |
| 726 | } |
| 727 | for (const prop of yamlPathProperties) { |
| 728 | setYamlPath(frontmatter, prop.path, prop.content, {createParents: false, createLeaf: false}); |
| 729 | } |
| 730 | }); |