1371 lines
59 KiB
JavaScript
Executable File
1371 lines
59 KiB
JavaScript
Executable File
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var __publicField = (obj, key, value) => {
|
|
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
return value;
|
|
};
|
|
|
|
// src/main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
MetadataInputModal: () => MetadataInputModal,
|
|
default: () => TimelinePlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian6 = require("obsidian");
|
|
|
|
// src/i18n.ts
|
|
var zhCN = {
|
|
settings: {
|
|
resetSettings: "\u91CD\u7F6E\u8BBE\u7F6E",
|
|
resetSettingsDesc: "\u5C06\u6240\u6709\u8BBE\u7F6E\u6062\u590D\u4E3A\u9ED8\u8BA4\u503C",
|
|
resetSettingsButton: "\u91CD\u7F6E\u4E3A\u9ED8\u8BA4\u503C",
|
|
lineWidth: "\u65F6\u95F4\u8F74\u7EBF\u5BBD\u5EA6",
|
|
lineWidthDesc: "\u8BBE\u7F6E\u4E3B\u65F6\u95F4\u8F74\u7EBF\u7684\u5BBD\u5EA6\uFF08\u50CF\u7D20\uFF09",
|
|
lineColor: "\u65F6\u95F4\u8F74\u7EBF\u989C\u8272",
|
|
lineColorDesc: "\u8BBE\u7F6E\u65F6\u95F4\u8F74\u7EBF\u7684\u989C\u8272",
|
|
colorPickerPlaceholder: "\u70B9\u51FB\u5DE6\u4FA7\u8272\u76D8\u9009\u62E9\u989C\u8272",
|
|
itemSpacing: "\u9879\u76EE\u95F4\u8DDD",
|
|
itemSpacingDesc: "\u8BBE\u7F6E\u65F6\u95F4\u8F74\u9879\u76EE\u4E4B\u95F4\u7684\u95F4\u8DDD\uFF08\u50CF\u7D20\uFF09",
|
|
cardBackground: "\u5361\u7247\u80CC\u666F\u8272",
|
|
cardBackgroundDesc: "\u8BBE\u7F6E\u5185\u5BB9\u5361\u7247\u7684\u80CC\u666F\u989C\u8272",
|
|
cardTextColor: "\u5361\u7247\u6587\u5B57\u989C\u8272",
|
|
cardTextColorDesc: "\u8BBE\u7F6E\u666E\u901A\u4E8B\u4EF6\u5361\u7247\u7684\u6587\u5B57\u989C\u8272",
|
|
cardBorderColor: "\u5361\u7247\u8FB9\u6846\u989C\u8272",
|
|
cardBorderColorDesc: "\u8BBE\u7F6E\u666E\u901A\u4E8B\u4EF6\u5361\u7247\u7684\u8FB9\u6846\u989C\u8272",
|
|
milestoneCardBackground: "\u91CC\u7A0B\u7891\u5361\u7247\u80CC\u666F\u8272",
|
|
milestoneCardBackgroundDesc: "\u8BBE\u7F6E\u91CC\u7A0B\u7891\u4E8B\u4EF6\u5361\u7247\u7684\u80CC\u666F\u989C\u8272",
|
|
milestoneCardTextColor: "\u91CC\u7A0B\u7891\u5361\u7247\u6587\u5B57\u989C\u8272",
|
|
milestoneCardTextColorDesc: "\u8BBE\u7F6E\u91CC\u7A0B\u7891\u4E8B\u4EF6\u5361\u7247\u7684\u6587\u5B57\u989C\u8272",
|
|
milestoneCardBorderColor: "\u91CC\u7A0B\u7891\u5361\u7247\u8FB9\u6846\u989C\u8272",
|
|
milestoneCardBorderColorDesc: "\u8BBE\u7F6E\u91CC\u7A0B\u7891\u4E8B\u4EF6\u5361\u7247\u7684\u8FB9\u6846\u989C\u8272",
|
|
animationDuration: "\u52A8\u753B\u6301\u7EED\u65F6\u95F4",
|
|
animationDurationDesc: "\u8BBE\u7F6E\u60AC\u505C\u52A8\u753B\u7684\u6301\u7EED\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09",
|
|
dateAttribute: "\u65E5\u671F\u5C5E\u6027",
|
|
dateAttributeDesc: "\u9009\u62E9\u7528\u4E8E\u65F6\u95F4\u8F74\u6392\u5E8F\u7684 frontmatter \u65E5\u671F\u5C5E\u6027\uFF08\u5982\uFF1Acreated, updated, date \u7B49\uFF09",
|
|
fileNamePrefix: "\u6587\u4EF6\u540D\u524D\u7F00",
|
|
fileNamePrefixDesc: "\u8BBE\u7F6E\u751F\u6210\u7684\u65F6\u95F4\u8F74\u6587\u4EF6\u540D\u524D\u7F00",
|
|
fileNameSuffix: "\u6587\u4EF6\u540D\u540E\u7F00",
|
|
fileNameSuffixDesc: "\u8BBE\u7F6E\u751F\u6210\u7684\u65F6\u95F4\u8F74\u6587\u4EF6\u540D\u540E\u7F00",
|
|
defaultPosition: "\u9ED8\u8BA4\u4F4D\u7F6E",
|
|
defaultPositionDesc: "\u9009\u62E9\u65F6\u95F4\u8F74\u89C6\u56FE\u5728\u5DE6\u4FA7\u8FD8\u662F\u53F3\u4FA7\u8FB9\u680F\u663E\u793A",
|
|
leftSidebar: "\u5DE6\u4FA7\u8FB9\u680F",
|
|
rightSidebar: "\u53F3\u4FA7\u8FB9\u680F",
|
|
language: "\u8BED\u8A00",
|
|
languageDesc: "\u9009\u62E9\u63D2\u4EF6\u754C\u9762\u8BED\u8A00",
|
|
milestoneAttribute: "\u91CC\u7A0B\u7891\u5C5E\u6027",
|
|
milestoneAttributeDesc: "\u7528\u4E8E\u6807\u8BB0\u91CC\u7A0B\u7891\u4E8B\u4EF6\u7684 frontmatter \u5C5E\u6027\u540D\u79F0\uFF08\u4F8B\u5982\uFF1Amilestone\uFF09",
|
|
milestoneValue: "\u91CC\u7A0B\u7891\u503C",
|
|
milestoneValueDesc: "\u91CC\u7A0B\u7891\u5C5E\u6027\u9700\u8981\u5339\u914D\u7684\u503C\uFF08\u7559\u7A7A\u5219\u8868\u793A\u53EA\u8981\u5C5E\u6027\u5B58\u5728\u5373\u53EF\uFF0C\u4F8B\u5982\uFF1Atrue, 1\uFF09"
|
|
},
|
|
commands: {
|
|
openTimelineView: "\u6253\u5F00\u65F6\u95F4\u8F74\u89C6\u56FE",
|
|
generateFromFolder: "\u4ECE\u6587\u4EF6\u5939\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE",
|
|
generateFileFromFolder: "\u4ECE\u6587\u4EF6\u5939\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6",
|
|
generateFromTag: "\u4ECE\u6807\u7B7E\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE",
|
|
generateFileFromTag: "\u4ECE\u6807\u7B7E\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6",
|
|
generateFromFileLinks: "\u4ECE\u5F53\u524D\u6587\u4EF6\u94FE\u63A5\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE",
|
|
generateFileFromFileLinks: "\u4ECE\u5F53\u524D\u6587\u4EF6\u94FE\u63A5\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6",
|
|
generateFromMetadata: "\u4ECE\u5143\u6570\u636E\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE",
|
|
generateFileFromMetadata: "\u4ECE\u5143\u6570\u636E\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6"
|
|
},
|
|
modal: {
|
|
metadataInputTitle: "\u8F93\u5165\u5143\u6570\u636E\u5B57\u7B26\u4E32",
|
|
metadataQueryName: "\u5143\u6570\u636E\u67E5\u8BE2",
|
|
metadataQueryDesc: "\u4F8B\u5982\uFF1Aauthor: - \u6211 \u6216 status: ongoing",
|
|
metadataInputPlaceholder: "\u8F93\u5165\u5143\u6570\u636E...",
|
|
submitButton: "\u786E\u5B9A",
|
|
cancelButton: "\u53D6\u6D88",
|
|
emptyInputNotice: "\u8BF7\u8F93\u5165\u5143\u6570\u636E\u5B57\u7B26\u4E32"
|
|
},
|
|
errors: {
|
|
renderFailed: "\u65F6\u95F4\u8F74\u89C6\u56FE\u6E32\u67D3\u5931\u8D25",
|
|
generateFolderFailed: "\u751F\u6210\u6587\u4EF6\u5939\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519",
|
|
generateTagFailed: "\u751F\u6210\u6807\u7B7E\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519",
|
|
generateFileFailed: "\u4ECE\u6587\u4EF6\u94FE\u63A5\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519",
|
|
noTaggedFiles: "\u6CA1\u6709\u627E\u5230\u5305\u542B\u6807\u7B7E #{tag} \u53CA\u5176\u5B50\u6807\u7B7E\u7684\u6587\u4EF6",
|
|
noFileLinks: "\u6587\u4EF6 {filename} \u4E2D\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684\u94FE\u63A5\u6216\u65E5\u671F\u4FE1\u606F",
|
|
createViewFailed: "\u65E0\u6CD5\u521B\u5EFA\u65F6\u95F4\u8F74\u89C6\u56FE\uFF1A\u8BF7\u68C0\u67E5\u4FA7\u8FB9\u680F\u7A7A\u95F4",
|
|
noMetadataFiles: "\u6CA1\u6709\u627E\u5230\u4E0E\u5143\u6570\u636E\u67E5\u8BE2 \u201C{query}\u201D \u5339\u914D\u7684\u6587\u4EF6",
|
|
generateMetadataFailed: "\u4ECE\u5143\u6570\u636E\u751F\u6210\u65F6\u95F4\u8F74\u5931\u8D25"
|
|
}
|
|
};
|
|
var enUS = {
|
|
settings: {
|
|
resetSettings: "Reset Settings",
|
|
resetSettingsDesc: "Restore all settings to default values",
|
|
resetSettingsButton: "Reset to Defaults",
|
|
lineWidth: "Timeline Line Width",
|
|
lineWidthDesc: "Set the width of the main timeline line (pixels)",
|
|
lineColor: "Timeline Line Color",
|
|
lineColorDesc: "Set the color of the timeline line",
|
|
colorPickerPlaceholder: "Click the color picker on the left",
|
|
itemSpacing: "Item Spacing",
|
|
itemSpacingDesc: "Set the spacing between timeline items (pixels)",
|
|
cardBackground: "Card Background Color",
|
|
cardBackgroundDesc: "Set the background color of content cards",
|
|
cardTextColor: "Card Text Color",
|
|
cardTextColorDesc: "Set the text color of regular event cards",
|
|
cardBorderColor: "Card Border Color",
|
|
cardBorderColorDesc: "Set the border color of regular event cards",
|
|
milestoneCardBackground: "Milestone Card Background",
|
|
milestoneCardBackgroundDesc: "Set the background color of milestone event cards",
|
|
milestoneCardTextColor: "Milestone Card Text Color",
|
|
milestoneCardTextColorDesc: "Set the text color of milestone event cards",
|
|
milestoneCardBorderColor: "Milestone Card Border Color",
|
|
milestoneCardBorderColorDesc: "Set the border color of milestone event cards",
|
|
animationDuration: "Animation Duration",
|
|
animationDurationDesc: "Set the duration of hover animations (milliseconds)",
|
|
dateAttribute: "Date Attribute",
|
|
dateAttributeDesc: "Choose the frontmatter date attribute for timeline sorting (e.g., created, updated, date)",
|
|
fileNamePrefix: "File Name Prefix",
|
|
fileNamePrefixDesc: "Set the prefix for generated timeline file names",
|
|
fileNameSuffix: "File Name Suffix",
|
|
fileNameSuffixDesc: "Set the suffix for generated timeline file names",
|
|
defaultPosition: "Default Position",
|
|
defaultPositionDesc: "Choose whether to display the timeline view in the left or right sidebar",
|
|
leftSidebar: "Left Sidebar",
|
|
rightSidebar: "Right Sidebar",
|
|
language: "Language",
|
|
languageDesc: "Select plugin interface language",
|
|
milestoneAttribute: "Milestone Attribute",
|
|
milestoneAttributeDesc: "The frontmatter attribute name used to mark milestone events (e.g., milestone)",
|
|
milestoneValue: "Milestone Value",
|
|
milestoneValueDesc: "The value the milestone attribute needs to match (leave empty to match if the attribute exists, e.g., true, 1)"
|
|
},
|
|
commands: {
|
|
openTimelineView: "Open Timeline View",
|
|
generateFromFolder: "Generate Timeline View from any Folder",
|
|
generateFileFromFolder: "Generate Timeline File from any Folder",
|
|
generateFromTag: "Generate Timeline View from any Tag",
|
|
generateFileFromTag: "Generate Timeline File from any Tag",
|
|
generateFromFileLinks: "Generate Timeline View from Current File Links",
|
|
generateFileFromFileLinks: "Generate Timeline File from Current File Links",
|
|
generateFromMetadata: "Generate Timeline View from any Metadata",
|
|
generateFileFromMetadata: "Generate Timeline File from any Metadata"
|
|
},
|
|
modal: {
|
|
metadataInputTitle: "Enter Metadata String",
|
|
metadataQueryName: "Metadata Query",
|
|
metadataQueryDesc: "e.g., author: - Me or status: ongoing",
|
|
metadataInputPlaceholder: "Enter metadata...",
|
|
submitButton: "Submit",
|
|
cancelButton: "Cancel",
|
|
emptyInputNotice: "Please enter a metadata string"
|
|
},
|
|
errors: {
|
|
renderFailed: "Failed to render timeline view",
|
|
generateFolderFailed: "Error generating folder timeline view",
|
|
generateTagFailed: "Error generating tag timeline view",
|
|
generateFileFailed: "Error generating file links timeline view",
|
|
noTaggedFiles: "No files found with tag #{tag} or its subtags",
|
|
noFileLinks: "No usable links or date information found in file {filename}",
|
|
createViewFailed: "Cannot create timeline view: Please check sidebar space",
|
|
noMetadataFiles: 'No files found matching metadata query "{query}"',
|
|
generateMetadataFailed: "Failed to generate timeline from metadata"
|
|
}
|
|
};
|
|
|
|
// src/TimelineView.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// src/Timeline.ts
|
|
var import_obsidian = require("obsidian");
|
|
var Timeline = class {
|
|
constructor(app, settings) {
|
|
__publicField(this, "app");
|
|
__publicField(this, "settings");
|
|
this.app = app;
|
|
this.settings = settings;
|
|
}
|
|
// 辅助函数:检查文件是否在timelines文件夹中
|
|
isInTimelinesFolder(file) {
|
|
return file.path.startsWith("timelines/") || file.path === "timelines";
|
|
}
|
|
async getFilePreview(file, tag) {
|
|
const content = await this.app.vault.cachedRead(file);
|
|
const normalizedTag = tag.replace(/^#/, "");
|
|
const tagLines = content.split("\n").filter((line) => {
|
|
const tagRegex = new RegExp(`(^|s)#${normalizedTag}(?=s|$|[^w/])`);
|
|
return tagRegex.test(line);
|
|
});
|
|
if (tagLines.length > 0) {
|
|
const firstTagLine = tagLines[0];
|
|
return firstTagLine.slice(0, 100) + (firstTagLine.length > 100 ? "..." : "");
|
|
}
|
|
const previewContent = content.replace(/^---[\s\S]*?---/, "").trim();
|
|
return previewContent.slice(0, 50) + (previewContent.length > 50 ? "..." : "");
|
|
}
|
|
async createTimelineItem(file, tag, metadataQuery) {
|
|
var _a;
|
|
const metadata = this.app.metadataCache.getFileCache(file);
|
|
const dateValue = (_a = metadata == null ? void 0 : metadata.frontmatter) == null ? void 0 : _a[this.settings.dateAttribute];
|
|
if (dateValue) {
|
|
const preview = tag ? await this.getFilePreview(file, tag) : await this.getFilePreview(file, "");
|
|
return {
|
|
date: new Date(dateValue),
|
|
title: file.basename,
|
|
path: file.path,
|
|
preview,
|
|
isMilestone: this.checkMilestone(metadata == null ? void 0 : metadata.frontmatter)
|
|
// 检查是否为里程碑
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
checkMilestone(frontmatter) {
|
|
if (!this.settings.milestoneAttribute || !frontmatter) {
|
|
return false;
|
|
}
|
|
const attributeValue = frontmatter[this.settings.milestoneAttribute];
|
|
if (attributeValue === void 0 || attributeValue === null) {
|
|
return false;
|
|
}
|
|
if (this.settings.milestoneValue === "") {
|
|
return true;
|
|
}
|
|
return String(attributeValue).toLowerCase() === String(this.settings.milestoneValue).toLowerCase();
|
|
}
|
|
sortItemsByDate(items) {
|
|
return items.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
}
|
|
extractLinkedFiles(content) {
|
|
const linkedFiles = [];
|
|
const linkRegex = /\[\[([^\]]+)\]\]/g;
|
|
let match;
|
|
while ((match = linkRegex.exec(content)) !== null) {
|
|
const linkPath = match[1].split("|")[0];
|
|
const file = this.app.metadataCache.getFirstLinkpathDest(linkPath, "");
|
|
if (file && file instanceof import_obsidian.TFile) {
|
|
linkedFiles.push(file);
|
|
}
|
|
}
|
|
return linkedFiles;
|
|
}
|
|
async generateFromFileLinks(file) {
|
|
const content = await this.app.vault.cachedRead(file);
|
|
const linkedFiles = this.extractLinkedFiles(content);
|
|
const items = [];
|
|
for (const linkedFile of linkedFiles) {
|
|
if (!this.isInTimelinesFolder(linkedFile)) {
|
|
const item = await this.createTimelineItem(linkedFile);
|
|
if (item) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
}
|
|
return this.sortItemsByDate(items);
|
|
}
|
|
async generateFromMetadata(metadataQuery) {
|
|
const allFiles = this.app.vault.getMarkdownFiles();
|
|
const filteredFiles = allFiles.filter((file) => !this.isInTimelinesFolder(file));
|
|
const items = [];
|
|
let queryKey;
|
|
let queryValue = null;
|
|
const firstColonIndex = metadataQuery.indexOf(":");
|
|
if (firstColonIndex > 0) {
|
|
queryKey = metadataQuery.substring(0, firstColonIndex).trim();
|
|
queryValue = metadataQuery.substring(firstColonIndex + 1).trim();
|
|
if (queryValue === "") {
|
|
queryValue = null;
|
|
}
|
|
} else {
|
|
queryKey = metadataQuery.trim();
|
|
}
|
|
for (const file of filteredFiles) {
|
|
try {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache || !cache.frontmatter)
|
|
continue;
|
|
const frontmatter = cache.frontmatter;
|
|
let match = false;
|
|
if (queryValue !== null) {
|
|
const fmValue = frontmatter[queryKey];
|
|
if (fmValue !== void 0 && fmValue !== null) {
|
|
if (Array.isArray(fmValue)) {
|
|
const cleanedQueryValue = queryValue.startsWith("- ") ? queryValue.substring(2) : queryValue;
|
|
match = fmValue.some((item) => {
|
|
const itemStr = String(item).trim().toLowerCase();
|
|
const queryStr = cleanedQueryValue.toLowerCase();
|
|
return itemStr === queryStr || itemStr.includes(queryStr) || queryStr.includes(itemStr);
|
|
});
|
|
} else {
|
|
const fmValueStr = String(fmValue).trim().toLowerCase();
|
|
const queryValueStr = queryValue.toLowerCase();
|
|
match = fmValueStr === queryValueStr || fmValueStr.includes(queryValueStr) || queryValueStr.includes(fmValueStr);
|
|
}
|
|
}
|
|
} else {
|
|
match = Object.prototype.hasOwnProperty.call(frontmatter, queryKey);
|
|
}
|
|
if (match) {
|
|
const item = await this.createTimelineItem(file, void 0, metadataQuery);
|
|
if (item) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`\u5904\u7406\u6587\u4EF6 ${file.path} \u65F6\u51FA\u9519:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
return this.sortItemsByDate(items);
|
|
}
|
|
async generateFromFolder(folder) {
|
|
if (folder.path === "timelines" || folder.path.startsWith("timelines/")) {
|
|
return [];
|
|
}
|
|
const timelineItems = [];
|
|
const processFolder = async (currentFolder) => {
|
|
var _a;
|
|
if (currentFolder.path === "timelines" || currentFolder.path.startsWith("timelines/")) {
|
|
return;
|
|
}
|
|
for (const item of currentFolder.children) {
|
|
if (item instanceof import_obsidian.TFile && item.extension === "md") {
|
|
const metadata = this.app.metadataCache.getFileCache(item);
|
|
const dateValue = (_a = metadata == null ? void 0 : metadata.frontmatter) == null ? void 0 : _a[this.settings.dateAttribute];
|
|
if (dateValue) {
|
|
timelineItems.push({
|
|
date: new Date(dateValue),
|
|
title: item.basename,
|
|
path: item.path,
|
|
preview: await this.getFilePreview(item, ""),
|
|
isMilestone: this.checkMilestone(metadata == null ? void 0 : metadata.frontmatter)
|
|
});
|
|
}
|
|
} else if (item instanceof import_obsidian.TFolder) {
|
|
await processFolder(item);
|
|
}
|
|
}
|
|
};
|
|
await processFolder(folder);
|
|
return timelineItems.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
}
|
|
async generateFromTag(tag) {
|
|
var _a;
|
|
const allFiles = this.app.vault.getMarkdownFiles();
|
|
const filteredFiles = allFiles.filter((file) => !this.isInTimelinesFolder(file));
|
|
const items = [];
|
|
const normalizedTag = tag.replace(/^#/, "").trim();
|
|
const isMatchingTag = (fileTag, searchTag) => {
|
|
const cleanFileTag = fileTag.replace(/^#/, "").trim().toLowerCase();
|
|
const cleanSearchTag = searchTag.toLowerCase();
|
|
return cleanFileTag === cleanSearchTag || cleanFileTag.startsWith(cleanSearchTag + "/") || cleanSearchTag.includes(cleanFileTag);
|
|
};
|
|
for (const file of filteredFiles) {
|
|
try {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache)
|
|
continue;
|
|
let hasMatchingTag = false;
|
|
if (cache.tags && cache.tags.length > 0) {
|
|
hasMatchingTag = cache.tags.some((tagObj) => {
|
|
return isMatchingTag(tagObj.tag, normalizedTag);
|
|
});
|
|
}
|
|
if (!hasMatchingTag && ((_a = cache.frontmatter) == null ? void 0 : _a.tags)) {
|
|
const frontmatterTags = cache.frontmatter.tags;
|
|
if (Array.isArray(frontmatterTags)) {
|
|
hasMatchingTag = frontmatterTags.some((frontmatterTag) => {
|
|
if (typeof frontmatterTag !== "string")
|
|
return false;
|
|
return isMatchingTag(frontmatterTag, normalizedTag);
|
|
});
|
|
} else if (typeof frontmatterTags === "string") {
|
|
const normalizedTags = frontmatterTags.replace(/[,;]/g, " ").trim();
|
|
const tagList = normalizedTags.split(/\s+/);
|
|
hasMatchingTag = tagList.some((tagItem) => {
|
|
return isMatchingTag(tagItem, normalizedTag);
|
|
});
|
|
}
|
|
}
|
|
if (hasMatchingTag) {
|
|
const item = await this.createTimelineItem(file, normalizedTag);
|
|
if (item) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`\u5904\u7406\u6587\u4EF6 ${file.path} \u65F6\u51FA\u9519:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
return this.sortItemsByDate(items);
|
|
}
|
|
async generateTimelineMarkdown(items, title, source) {
|
|
let generatedFromValue = source.value;
|
|
if (source.type === "metadata") {
|
|
const parts = source.value.split(":");
|
|
const key = parts[0].trim();
|
|
const value = parts.slice(1).join(":").trim();
|
|
generatedFromValue = `${key}:${value}`;
|
|
}
|
|
let markdown = `---
|
|
generated_from: ${source.type}:${generatedFromValue}
|
|
---
|
|
|
|
`;
|
|
markdown += `# ${title}
|
|
|
|
`;
|
|
let currentYear = null;
|
|
let currentMonth = null;
|
|
for (const item of items) {
|
|
const date = item.date;
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth();
|
|
if (currentYear !== year) {
|
|
markdown += `
|
|
## ${year}
|
|
|
|
`;
|
|
currentYear = year;
|
|
currentMonth = null;
|
|
}
|
|
if (currentMonth !== month) {
|
|
const monthName = this.settings.language === "zh-CN" ? date.toLocaleString("zh-CN", { month: "long" }) : date.toLocaleString("en-US", { month: "long" });
|
|
markdown += `
|
|
### ${monthName}
|
|
|
|
`;
|
|
currentMonth = month;
|
|
}
|
|
const dateFormat = this.settings.language === "zh-CN" ? date.toLocaleDateString("zh-CN") : date.toLocaleDateString("en-US");
|
|
markdown += `#### ${dateFormat} - [[${item.title}]]
|
|
|
|
`;
|
|
}
|
|
return markdown;
|
|
}
|
|
};
|
|
|
|
// src/TimelineView.ts
|
|
var VIEW_TYPE_TIMELINE = "timeline-view";
|
|
var CLASS_TIMELINE_HEADER = "timeline-header";
|
|
var CLASS_TIMELINE_CONTAINER = "timeline-container";
|
|
var CLASS_TIMELINE_LINE = "timeline-line";
|
|
var CLASS_TIMELINE_ERA = "timeline-era";
|
|
var CLASS_TIMELINE_ERA_TITLE = "timeline-era-title";
|
|
var CLASS_TIMELINE_ITEM = "timeline-item";
|
|
var CLASS_TIMELINE_CARD = "timeline-card";
|
|
var CLASS_TIMELINE_DATE = "timeline-date";
|
|
var CLASS_TIMELINE_TITLE = "timeline-title";
|
|
var CLASS_TIMELINE_PREVIEW = "timeline-preview";
|
|
var TimelineView = class extends import_obsidian2.ItemView {
|
|
constructor(leaf, settings, i18n) {
|
|
super(leaf);
|
|
__publicField(this, "timeline");
|
|
__publicField(this, "settings");
|
|
__publicField(this, "i18n");
|
|
__publicField(this, "items", []);
|
|
__publicField(this, "currentTitle", "");
|
|
this.timeline = new Timeline(this.app, settings);
|
|
this.settings = settings;
|
|
this.i18n = i18n;
|
|
}
|
|
getViewType() {
|
|
return VIEW_TYPE_TIMELINE;
|
|
}
|
|
getDisplayText() {
|
|
return this.i18n.commands.openTimelineView;
|
|
}
|
|
async onOpen() {
|
|
try {
|
|
await this.render();
|
|
} catch (error) {
|
|
new import_obsidian2.Notice(this.i18n.errors.renderFailed);
|
|
}
|
|
}
|
|
async updateFromMetadata(metadataQuery) {
|
|
try {
|
|
this.currentTitle = `\u{1F50D} ${metadataQuery}`;
|
|
this.items = await this.timeline.generateFromMetadata(metadataQuery);
|
|
if (this.items.length === 0) {
|
|
new import_obsidian2.Notice(this.i18n.errors.noMetadataFiles.replace("{query}", metadataQuery));
|
|
return;
|
|
}
|
|
await this.render();
|
|
} catch (error) {
|
|
new import_obsidian2.Notice(this.i18n.errors.generateMetadataFailed);
|
|
console.error("Error generating timeline from metadata:", error);
|
|
}
|
|
}
|
|
renderTitle(container) {
|
|
const titleContainer = container.createEl("div", { cls: CLASS_TIMELINE_HEADER });
|
|
titleContainer.createEl("h2", { text: this.currentTitle });
|
|
}
|
|
groupItemsByYear() {
|
|
const itemsByYear = /* @__PURE__ */ new Map();
|
|
this.items.forEach((item) => {
|
|
var _a;
|
|
const year = item.date.getFullYear();
|
|
if (!itemsByYear.has(year)) {
|
|
itemsByYear.set(year, []);
|
|
}
|
|
(_a = itemsByYear.get(year)) == null ? void 0 : _a.push(item);
|
|
});
|
|
return itemsByYear;
|
|
}
|
|
renderItem(container, item, index, itemsLength) {
|
|
const itemClasses = [`${CLASS_TIMELINE_ITEM}${this.getItemClasses(index, itemsLength)}`];
|
|
if (item.isMilestone) {
|
|
itemClasses.push("timeline-item-milestone");
|
|
}
|
|
const itemEl = container.createEl("div", {
|
|
cls: itemClasses.join(" "),
|
|
attr: { style: `--timeline-line-color: ${this.settings.lineColor}` }
|
|
});
|
|
const card = itemEl.createEl("div", { cls: CLASS_TIMELINE_CARD });
|
|
card.createEl("div", {
|
|
cls: CLASS_TIMELINE_DATE,
|
|
text: item.date.toLocaleDateString(this.settings.language === "zh-CN" ? "zh-CN" : "en-US")
|
|
});
|
|
const titleEl = card.createEl("div", {
|
|
cls: CLASS_TIMELINE_TITLE,
|
|
attr: { style: `color: ${item.isMilestone ? this.settings.milestoneCardTextColor : this.settings.cardTextColor} !important` }
|
|
});
|
|
titleEl.textContent = item.title;
|
|
titleEl.dataset.colorSetting = item.isMilestone ? "milestoneCardTextColor" : "cardTextColor";
|
|
if (item.preview) {
|
|
const previewEl = card.createEl("div", {
|
|
cls: CLASS_TIMELINE_PREVIEW,
|
|
attr: { style: `color: ${item.isMilestone ? this.settings.milestoneCardTextColor : this.settings.cardTextColor} !important` }
|
|
});
|
|
previewEl.textContent = item.preview;
|
|
previewEl.dataset.colorSetting = item.isMilestone ? "milestoneCardTextColor" : "cardTextColor";
|
|
}
|
|
titleEl.addEventListener("click", async () => {
|
|
const file = this.app.vault.getAbstractFileByPath(item.path);
|
|
if (file instanceof import_obsidian2.TFile) {
|
|
await this.app.workspace.getLeaf().openFile(file);
|
|
}
|
|
});
|
|
}
|
|
getItemClasses(index, itemsLength) {
|
|
return (index === 0 ? " first-item" : "") + (index === itemsLength - 1 ? " last-item" : "");
|
|
}
|
|
renderEra(container, year, items) {
|
|
const era = container.createEl("div", { cls: CLASS_TIMELINE_ERA });
|
|
era.createEl("div", { cls: CLASS_TIMELINE_ERA_TITLE, text: `${year}` });
|
|
items.forEach((item, index) => this.renderItem(era, item, index, items.length));
|
|
}
|
|
async render() {
|
|
const container = this.containerEl.children[1];
|
|
container.empty();
|
|
if (this.currentTitle) {
|
|
this.renderTitle(container);
|
|
}
|
|
const timelineContainer = container.createEl("div", { cls: CLASS_TIMELINE_CONTAINER });
|
|
timelineContainer.createEl("div", { cls: CLASS_TIMELINE_LINE });
|
|
const itemsByYear = this.groupItemsByYear();
|
|
Array.from(itemsByYear.entries()).sort(([yearA], [yearB]) => yearB - yearA).forEach(([year, items]) => this.renderEra(timelineContainer, year, items));
|
|
}
|
|
async updateFromFolder(folderPath) {
|
|
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (folder) {
|
|
this.currentTitle = `\u{1F4C2} ${folder.name}`;
|
|
this.items = await this.timeline.generateFromFolder(folder);
|
|
await this.render();
|
|
}
|
|
}
|
|
async updateFromTag(tag) {
|
|
try {
|
|
this.currentTitle = `\u{1F3F7}\uFE0F ${tag}`;
|
|
this.items = await this.timeline.generateFromTag(tag);
|
|
if (this.items.length === 0) {
|
|
new import_obsidian2.Notice(this.i18n.errors.noTaggedFiles.replace("{tag}", tag));
|
|
return;
|
|
}
|
|
await this.render();
|
|
} catch (error) {
|
|
new import_obsidian2.Notice("\u751F\u6210\u65F6\u95F4\u8F74\u5931\u8D25");
|
|
throw error;
|
|
}
|
|
}
|
|
async updateFromFile(filePath) {
|
|
try {
|
|
const file = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (file && file instanceof import_obsidian2.TFile) {
|
|
this.currentTitle = `\u{1F4C4} ${file.basename}`;
|
|
this.items = await this.timeline.generateFromFileLinks(file);
|
|
if (this.items.length === 0) {
|
|
new import_obsidian2.Notice(this.i18n.errors.noFileLinks.replace("{filename}", file.basename));
|
|
return;
|
|
}
|
|
await this.render();
|
|
}
|
|
} catch (error) {
|
|
new import_obsidian2.Notice(this.i18n.errors.generateFileFailed);
|
|
console.error(error);
|
|
}
|
|
}
|
|
getIcon() {
|
|
return "history";
|
|
}
|
|
};
|
|
|
|
// src/FolderSuggest.ts
|
|
var import_obsidian3 = require("obsidian");
|
|
var FolderSuggestModal = class extends import_obsidian3.FuzzySuggestModal {
|
|
constructor(app, settings) {
|
|
super(app);
|
|
this.settings = settings;
|
|
__publicField(this, "resolve");
|
|
this.setPlaceholder(this.settings.language === "zh-CN" ? "\u9009\u62E9\u4E00\u4E2A\u6587\u4EF6\u5939" : "Select a folder");
|
|
}
|
|
getItems() {
|
|
const folders = [];
|
|
const shouldFilterFolder = (folderPath) => {
|
|
return folderPath === "timelines" || folderPath.startsWith("timelines/");
|
|
};
|
|
const pushFolder = (folder) => {
|
|
if (!shouldFilterFolder(folder.path)) {
|
|
folders.push(folder);
|
|
folder.children.filter((child) => child instanceof import_obsidian3.TFolder).forEach(pushFolder);
|
|
}
|
|
};
|
|
const rootFolder = this.app.vault.getRoot();
|
|
pushFolder(rootFolder);
|
|
return folders;
|
|
}
|
|
getItemText(folder) {
|
|
return folder.path;
|
|
}
|
|
onChooseItem(folder) {
|
|
this.resolve(folder.path);
|
|
this.close();
|
|
}
|
|
async openAndGetValue() {
|
|
return new Promise((resolve) => {
|
|
this.resolve = resolve;
|
|
this.open();
|
|
}).then((value) => {
|
|
if (value === void 0) {
|
|
return null;
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/TagSuggest.ts
|
|
var import_obsidian4 = require("obsidian");
|
|
var TagSuggestModal = class extends import_obsidian4.FuzzySuggestModal {
|
|
constructor(app, settings) {
|
|
super(app);
|
|
this.settings = settings;
|
|
__publicField(this, "resolve");
|
|
this.setPlaceholder(this.settings.language === "zh-CN" ? "\u9009\u62E9\u4E00\u4E2A\u6807\u7B7E" : "Select a tag");
|
|
}
|
|
getItems() {
|
|
const tags = /* @__PURE__ */ new Set();
|
|
const files = this.app.vault.getMarkdownFiles();
|
|
files.forEach((file) => {
|
|
var _a, _b;
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
(_a = cache == null ? void 0 : cache.tags) == null ? void 0 : _a.forEach((tag) => {
|
|
tags.add(tag.tag.replace("#", ""));
|
|
});
|
|
const frontmatterTags = (_b = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _b.tags;
|
|
if (Array.isArray(frontmatterTags)) {
|
|
frontmatterTags.forEach((tag) => {
|
|
tags.add(tag.replace("#", ""));
|
|
});
|
|
} else if (typeof frontmatterTags === "string") {
|
|
const normalizedTags = frontmatterTags.replace(/[,;]/g, " ").trim();
|
|
normalizedTags.split(/\s+/).forEach((tag) => {
|
|
if (tag) {
|
|
tags.add(tag.trim().replace("#", ""));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
const tagArray = Array.from(tags);
|
|
console.log("\u53EF\u7528\u7684\u6807\u7B7E:", tagArray);
|
|
return tagArray;
|
|
}
|
|
getItemText(tag) {
|
|
return tag;
|
|
}
|
|
onChooseItem(tag) {
|
|
console.log("\u9009\u62E9\u7684\u6807\u7B7E:", tag);
|
|
this.resolve(tag);
|
|
this.close();
|
|
}
|
|
async openAndGetValue() {
|
|
return new Promise((resolve) => {
|
|
this.resolve = resolve;
|
|
this.open();
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/FileSuggest.ts
|
|
var import_obsidian5 = require("obsidian");
|
|
var FileSuggestModal = class extends import_obsidian5.FuzzySuggestModal {
|
|
constructor(app, settings) {
|
|
super(app);
|
|
this.settings = settings;
|
|
__publicField(this, "resolve");
|
|
this.setPlaceholder(this.settings.language === "zh-CN" ? "\u9009\u62E9\u4E00\u4E2AMarkdown\u6587\u4EF6" : "Select a Markdown file");
|
|
}
|
|
async openAndGetValue() {
|
|
return new Promise((resolve) => {
|
|
this.resolve = resolve;
|
|
this.open();
|
|
});
|
|
}
|
|
getItems() {
|
|
return this.app.vault.getMarkdownFiles().filter(
|
|
(file) => !file.path.startsWith("timelines/") && file.path !== "timelines"
|
|
);
|
|
}
|
|
getItemText(file) {
|
|
return file.path;
|
|
}
|
|
onChooseItem(file) {
|
|
this.resolve(file);
|
|
this.close();
|
|
}
|
|
};
|
|
|
|
// src/TimelineSettings.ts
|
|
function getCssVariable(name) {
|
|
return `var(${name})`;
|
|
}
|
|
var DEFAULT_SETTINGS = {
|
|
lineWidth: 2,
|
|
lineColor: getCssVariable("--interactive-accent"),
|
|
itemSpacing: 30,
|
|
cardBackground: getCssVariable("--background-primary-alt"),
|
|
cardTextColor: getCssVariable("--text-normal"),
|
|
cardBorderColor: getCssVariable("--background-modifier-border"),
|
|
milestoneCardBackground: "#ffffcc",
|
|
milestoneCardTextColor: getCssVariable("--text-normal"),
|
|
milestoneCardBorderColor: "#f0e68c",
|
|
animationDuration: 200,
|
|
dateAttribute: "created",
|
|
// 默认使用 frontmatter 中的 date 属性作为时间轴排序依据
|
|
fileNamePrefix: "",
|
|
fileNameSuffix: "",
|
|
defaultPosition: "right",
|
|
// 默认在右侧边栏
|
|
language: "zh-CN",
|
|
// 默认使用中文
|
|
milestoneAttribute: "",
|
|
// 默认不使用里程碑属性
|
|
milestoneValue: ""
|
|
// 默认值为空
|
|
};
|
|
|
|
// src/main.ts
|
|
var import_obsidian7 = require("obsidian");
|
|
var TimelinePlugin = class extends import_obsidian6.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
__publicField(this, "settings");
|
|
__publicField(this, "i18n");
|
|
}
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.i18n = this.settings.language === "zh-CN" ? zhCN : enUS;
|
|
this.addSettingTab(new TimelineSettingTab(this.app, this));
|
|
console.log("\u52A0\u8F7D Timeline \u63D2\u4EF6");
|
|
try {
|
|
this.registerView(
|
|
VIEW_TYPE_TIMELINE,
|
|
(leaf) => new TimelineView(leaf, this.settings, this.i18n)
|
|
);
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", (menu, abstractFile) => {
|
|
var _a;
|
|
if (abstractFile instanceof import_obsidian6.TFile && abstractFile.path.startsWith("timelines/") && abstractFile.extension === "md") {
|
|
const cache = this.app.metadataCache.getFileCache(abstractFile);
|
|
const generatedFrom = (_a = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _a.generated_from;
|
|
if (generatedFrom) {
|
|
const [type, ...valueParts] = generatedFrom.split(":");
|
|
const value = valueParts.join(":");
|
|
menu.addItem((item) => {
|
|
item.setTitle(this.i18n.commands.openTimelineView).setIcon("clock").onClick(async () => {
|
|
await this.activateView(this.settings.defaultPosition);
|
|
const view = this.getTimelineView();
|
|
if (view) {
|
|
if (type === "folder") {
|
|
await view.updateFromFolder(value);
|
|
} else if (type === "tag") {
|
|
await view.updateFromTag(value);
|
|
} else if (type === "file") {
|
|
await view.updateFromFile(value);
|
|
} else if (type === "metadata") {
|
|
await view.updateFromMetadata(value);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
})
|
|
);
|
|
this.addCommand({
|
|
id: "generate-timeline-view-from-folder",
|
|
name: this.i18n.commands.generateFromFolder,
|
|
callback: async () => {
|
|
try {
|
|
const folderPath = await this.selectFolder();
|
|
if (folderPath) {
|
|
await this.activateView(this.settings.defaultPosition);
|
|
const view = this.getTimelineView();
|
|
if (view) {
|
|
await view.updateFromFolder(folderPath);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("\u751F\u6210\u6587\u4EF6\u5939\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519:", error);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-file-from-folder",
|
|
name: this.i18n.commands.generateFileFromFolder,
|
|
callback: async () => {
|
|
try {
|
|
const folderPath = await this.selectFolder();
|
|
if (folderPath) {
|
|
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
|
if (!(folder instanceof import_obsidian7.TFolder)) {
|
|
throw new Error(`Invalid folder path: ${folderPath}`);
|
|
}
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
const items = await timeline.generateFromFolder(folder);
|
|
const content = await timeline.generateTimelineMarkdown(items, `Timeline - ${folderPath}`, { type: "folder", value: folderPath });
|
|
const { folderPath: targetFolder, fileName } = await this.createNestedFolders(folderPath);
|
|
const finalFileName = this.generateFileName(fileName || folderPath.split("/").pop() || "");
|
|
const filePath = `${targetFolder}/${finalFileName}.md`;
|
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (existingFile) {
|
|
await this.app.vault.delete(existingFile);
|
|
}
|
|
const file = await this.app.vault.create(filePath, content);
|
|
await this.app.workspace.getLeaf().openFile(file);
|
|
}
|
|
} catch (error) {
|
|
console.error("\u751F\u6210\u6587\u4EF6\u5939\u65F6\u95F4\u8F74\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-view-from-tag",
|
|
name: this.i18n.commands.generateFromTag,
|
|
callback: async () => {
|
|
try {
|
|
const tag = await this.selectTag();
|
|
if (tag) {
|
|
await this.activateView(this.settings.defaultPosition);
|
|
const view = this.getTimelineView();
|
|
if (view) {
|
|
await view.updateFromTag(tag);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("\u751F\u6210\u6807\u7B7E\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519:", error);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-file-from-tag",
|
|
name: this.i18n.commands.generateFileFromTag,
|
|
callback: async () => {
|
|
try {
|
|
const tag = await this.selectTag();
|
|
if (tag) {
|
|
await this.generateTimelineFileFromTag(tag);
|
|
}
|
|
} catch (error) {
|
|
console.error("\u751F\u6210\u6807\u7B7E\u65F6\u95F4\u8F74\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-view-from-file-links",
|
|
name: this.i18n.commands.generateFromFileLinks,
|
|
callback: async () => {
|
|
try {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file || !(file instanceof import_obsidian6.TFile) || file.extension !== "md") {
|
|
new import_obsidian6.Notice(this.i18n.errors.generateFileFailed);
|
|
return;
|
|
}
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache || !cache.links || cache.links.length === 0) {
|
|
new import_obsidian6.Notice(this.i18n.errors.noFileLinks.replace("{filename}", file.basename));
|
|
return;
|
|
}
|
|
await this.activateView(this.settings.defaultPosition);
|
|
const view = this.getTimelineView();
|
|
if (view) {
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
const items = await timeline.generateFromFileLinks(file);
|
|
view.items = items;
|
|
view.currentTitle = `\u{1F517} ${file.basename} `;
|
|
await view.render();
|
|
}
|
|
} catch (error) {
|
|
console.error("\u4ECE\u6587\u4EF6\u94FE\u63A5\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519:", error);
|
|
new import_obsidian6.Notice(this.i18n.errors.generateFileFailed);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-file-from-file-links",
|
|
name: this.i18n.commands.generateFileFromFileLinks,
|
|
callback: async () => {
|
|
try {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file || !(file instanceof import_obsidian6.TFile) || file.extension !== "md") {
|
|
new import_obsidian6.Notice(this.i18n.errors.generateFileFailed);
|
|
return;
|
|
}
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache || !cache.links || cache.links.length === 0) {
|
|
new import_obsidian6.Notice(this.i18n.errors.noFileLinks.replace("{filename}", file.basename));
|
|
return;
|
|
}
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
const items = await timeline.generateFromFileLinks(file);
|
|
const content = await timeline.generateTimelineMarkdown(
|
|
items,
|
|
`Timeline - Links in ${file.basename}`,
|
|
{ type: "file", value: file.path }
|
|
);
|
|
const { folderPath } = await this.createNestedFolders("linked-files");
|
|
const finalFileName = this.generateFileName(file.basename);
|
|
const filePath = `${folderPath}/${finalFileName}.md`;
|
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (existingFile) {
|
|
await this.app.vault.delete(existingFile);
|
|
}
|
|
const newFile = await this.app.vault.create(filePath, content);
|
|
await this.app.workspace.getLeaf().openFile(newFile);
|
|
} catch (error) {
|
|
console.error("\u4ECE\u6587\u4EF6\u94FE\u63A5\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
new import_obsidian6.Notice(this.i18n.errors.generateFileFailed);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-view-from-metadata",
|
|
name: this.i18n.commands.generateFromMetadata,
|
|
callback: async () => {
|
|
try {
|
|
const metadataQuery = await this.selectMetadataString();
|
|
if (metadataQuery) {
|
|
await this.activateView(this.settings.defaultPosition);
|
|
const view = this.getTimelineView();
|
|
if (view) {
|
|
await view.updateFromMetadata(metadataQuery);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("\u4ECE\u5143\u6570\u636E\u751F\u6210\u65F6\u95F4\u8F74\u89C6\u56FE\u65F6\u51FA\u9519:", error);
|
|
new import_obsidian6.Notice(this.i18n.errors.generateMetadataFailed);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "generate-timeline-file-from-metadata",
|
|
name: this.i18n.commands.generateFileFromMetadata,
|
|
callback: async () => {
|
|
try {
|
|
const metadataQuery = await this.selectMetadataString();
|
|
if (metadataQuery) {
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
const items = await timeline.generateFromMetadata(metadataQuery);
|
|
const content = await timeline.generateTimelineMarkdown(
|
|
items,
|
|
`Timeline - Metadata: ${metadataQuery}`,
|
|
{ type: "metadata", value: metadataQuery }
|
|
);
|
|
const { folderPath } = await this.createNestedFolders("metadata-search");
|
|
const safeBaseName = metadataQuery.substring(0, 30).replace(/[^a-zA-Z0-9_\-]+/g, "_") || "metadata_query";
|
|
const finalFileName = this.generateFileName(safeBaseName);
|
|
const filePath = `${folderPath}/${finalFileName}.md`;
|
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (existingFile) {
|
|
await this.app.vault.delete(existingFile);
|
|
}
|
|
const newFile = await this.app.vault.create(filePath, content);
|
|
await this.app.workspace.getLeaf().openFile(newFile);
|
|
}
|
|
} catch (error) {
|
|
console.error("\u4ECE\u5143\u6570\u636E\u751F\u6210\u65F6\u95F4\u8F74\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
new import_obsidian6.Notice(this.i18n.errors.generateMetadataFailed);
|
|
}
|
|
}
|
|
});
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-open", async (file) => {
|
|
var _a;
|
|
if (file instanceof import_obsidian6.TFile && file.path.startsWith("timelines/") && file.extension === "md") {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
const generatedFrom = (_a = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _a.generated_from;
|
|
if (generatedFrom) {
|
|
const [type, ...valueParts] = generatedFrom.split(":");
|
|
const value = valueParts.join(":");
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
let items = [];
|
|
if (type === "folder") {
|
|
const folder = this.app.vault.getAbstractFileByPath(value);
|
|
if (folder instanceof import_obsidian7.TFolder) {
|
|
items = await timeline.generateFromFolder(folder);
|
|
}
|
|
} else if (type === "tag") {
|
|
items = await timeline.generateFromTag(value);
|
|
} else if (type === "file") {
|
|
const targetFile = this.app.vault.getAbstractFileByPath(value);
|
|
if (targetFile instanceof import_obsidian6.TFile) {
|
|
items = await timeline.generateFromFileLinks(targetFile);
|
|
}
|
|
} else if (type === "metadata") {
|
|
items = await timeline.generateFromMetadata(value);
|
|
}
|
|
const content = await timeline.generateTimelineMarkdown(items, `Timeline - ${value}`, { type, value });
|
|
await this.app.vault.modify(file, content);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
} catch (error) {
|
|
console.error("\u63D2\u4EF6\u52A0\u8F7D\u65F6\u51FA\u9519:", error);
|
|
}
|
|
}
|
|
async selectFolder() {
|
|
const modal = new FolderSuggestModal(this.app, this.settings);
|
|
return await modal.openAndGetValue();
|
|
}
|
|
async selectFile() {
|
|
const modal = new FileSuggestModal(this.app, this.settings);
|
|
return await modal.openAndGetValue();
|
|
}
|
|
async selectTag() {
|
|
const modal = new TagSuggestModal(this.app, this.settings);
|
|
return await modal.openAndGetValue();
|
|
}
|
|
async selectMetadataString() {
|
|
const modal = new MetadataInputModal(this.app, this);
|
|
return await modal.openAndGetValue();
|
|
}
|
|
async activateView(position = this.settings.defaultPosition) {
|
|
const { workspace } = this.app;
|
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_TIMELINE);
|
|
if (leaves.length > 0) {
|
|
workspace.revealLeaf(leaves[0]);
|
|
return leaves[0];
|
|
}
|
|
const newLeaf = position === "left" ? workspace.getLeftLeaf(false) : workspace.getRightLeaf(false);
|
|
if (!newLeaf) {
|
|
new import_obsidian6.Notice("\u65E0\u6CD5\u521B\u5EFA\u65F6\u95F4\u8F74\u89C6\u56FE\uFF1A\u8BF7\u68C0\u67E5\u4FA7\u8FB9\u680F\u7A7A\u95F4");
|
|
return null;
|
|
}
|
|
await newLeaf.setViewState({ type: VIEW_TYPE_TIMELINE });
|
|
workspace.revealLeaf(newLeaf);
|
|
return newLeaf;
|
|
}
|
|
getTimelineView() {
|
|
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_TIMELINE);
|
|
if (leaves.length === 0) {
|
|
return null;
|
|
}
|
|
return leaves[0].view;
|
|
}
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
this.updateStyles();
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.updateStyles();
|
|
}
|
|
updateStyles() {
|
|
var _a;
|
|
const style = document.createElement("style");
|
|
style.id = "timeline-custom-styles";
|
|
style.textContent = `
|
|
.timeline-line {
|
|
width: ${this.settings.lineWidth}px !important;
|
|
background: ${this.settings.lineColor} !important;
|
|
}
|
|
.timeline-item::after {
|
|
background: var(--timeline-line-color, ${this.settings.lineColor}) !important;
|
|
}
|
|
.timeline-item {
|
|
margin: ${this.settings.itemSpacing}px 0 !important;
|
|
}
|
|
.timeline-card {
|
|
background: ${this.settings.cardBackground} !important;
|
|
color: ${this.settings.cardTextColor} !important;
|
|
border: 1px solid ${this.settings.cardBorderColor} !important;
|
|
transition: transform ${this.settings.animationDuration}ms !important;
|
|
}
|
|
.timeline-item-milestone .timeline-card {
|
|
background: ${this.settings.milestoneCardBackground} !important;
|
|
color: ${this.settings.milestoneCardTextColor} !important;
|
|
border: 1px solid ${this.settings.milestoneCardBorderColor} !important;
|
|
}
|
|
[data-color-setting="cardTextColor"] {
|
|
color: ${this.settings.cardTextColor} !important;
|
|
}
|
|
[data-color-setting="milestoneCardTextColor"] {
|
|
color: ${this.settings.milestoneCardTextColor} !important;
|
|
}
|
|
.timeline-date {
|
|
color: ${this.settings.cardTextColor} !important;
|
|
}
|
|
.timeline-item-milestone .timeline-date {
|
|
color: ${this.settings.milestoneCardTextColor} !important;
|
|
}
|
|
.timeline-era-title {
|
|
color: var(--timeline-line-color, ${this.settings.lineColor}) !important;
|
|
}
|
|
`;
|
|
(_a = document.getElementById("timeline-custom-styles")) == null ? void 0 : _a.remove();
|
|
document.head.appendChild(style);
|
|
}
|
|
async createNestedFolders(tagPath) {
|
|
const rootPath = "timelines";
|
|
if (!this.app.vault.getAbstractFileByPath(rootPath)) {
|
|
try {
|
|
await this.app.vault.createFolder(rootPath);
|
|
} catch (error) {
|
|
new import_obsidian6.Notice(`\u521B\u5EFA\u6839\u76EE\u5F55\u5931\u8D25: ${rootPath}`);
|
|
throw error;
|
|
}
|
|
}
|
|
const parts = tagPath.split("/");
|
|
const fileName = parts.pop();
|
|
let currentPath = rootPath;
|
|
for (const part of parts) {
|
|
currentPath = (0, import_obsidian6.normalizePath)(`${currentPath}/${part}`);
|
|
const folder = this.app.vault.getAbstractFileByPath(currentPath);
|
|
if (!folder) {
|
|
try {
|
|
await this.app.vault.createFolder(currentPath);
|
|
} catch (error) {
|
|
new import_obsidian6.Notice(`\u521B\u5EFA\u6587\u4EF6\u5939\u5931\u8D25: ${currentPath}`);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
return { folderPath: currentPath, fileName };
|
|
}
|
|
generateFileName(baseName) {
|
|
const prefix = this.settings.fileNamePrefix || "";
|
|
const suffix = this.settings.fileNameSuffix || "";
|
|
return `${prefix}${baseName}${suffix}`;
|
|
}
|
|
async generateTimelineFileFromTag(tag) {
|
|
try {
|
|
const tagPath = tag.replace("#", "");
|
|
const { folderPath, fileName } = await this.createNestedFolders(tagPath);
|
|
const finalFileName = this.generateFileName(fileName || "");
|
|
const filePath = `${folderPath}/${finalFileName}.md`;
|
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (existingFile) {
|
|
await this.app.vault.delete(existingFile);
|
|
}
|
|
const timeline = new Timeline(this.app, this.settings);
|
|
const items = await timeline.generateFromTag(tag);
|
|
const content = await timeline.generateTimelineMarkdown(items, `Timeline - ${tag}`, { type: "tag", value: tag });
|
|
const file = await this.app.vault.create(filePath, content);
|
|
await this.app.workspace.getLeaf().openFile(file);
|
|
} catch (error) {
|
|
console.error("\u751F\u6210\u6807\u7B7E\u65F6\u95F4\u8F74\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
var MetadataInputModal = class extends import_obsidian6.Modal {
|
|
constructor(app, plugin) {
|
|
super(app);
|
|
__publicField(this, "plugin");
|
|
// Add plugin reference
|
|
__publicField(this, "value", "");
|
|
__publicField(this, "resolvePromise");
|
|
__publicField(this, "inputEl");
|
|
this.plugin = plugin;
|
|
}
|
|
async openAndGetValue() {
|
|
return new Promise((resolve) => {
|
|
this.resolvePromise = resolve;
|
|
this.open();
|
|
});
|
|
}
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("metadata-input-modal");
|
|
contentEl.createEl("h2", { text: this.plugin.i18n.modal.metadataInputTitle });
|
|
new import_obsidian6.Setting(contentEl).setName(this.plugin.i18n.modal.metadataQueryName).setDesc(this.plugin.i18n.modal.metadataQueryDesc).addText((text) => {
|
|
this.inputEl = text;
|
|
text.setPlaceholder(this.plugin.i18n.modal.metadataInputPlaceholder).onChange((value) => this.value = value.trim());
|
|
text.inputEl.style.width = "100%";
|
|
text.inputEl.addEventListener("keydown", (evt) => {
|
|
if (evt.key === "Enter") {
|
|
evt.preventDefault();
|
|
this.submit();
|
|
}
|
|
});
|
|
});
|
|
new import_obsidian6.Setting(contentEl).addButton((button) => button.setButtonText(this.plugin.i18n.modal.submitButton).setCta().onClick(() => this.submit())).addButton((button) => button.setButtonText(this.plugin.i18n.modal.cancelButton).onClick(() => this.closeAndResolve(null)));
|
|
setTimeout(() => {
|
|
if (this.inputEl && this.inputEl.inputEl) {
|
|
this.inputEl.inputEl.focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
submit() {
|
|
if (this.value) {
|
|
this.closeAndResolve(this.value);
|
|
} else {
|
|
new import_obsidian6.Notice(this.plugin.i18n.modal.emptyInputNotice);
|
|
}
|
|
}
|
|
closeAndResolve(value) {
|
|
if (this.resolvePromise) {
|
|
this.resolvePromise(value);
|
|
}
|
|
this.close();
|
|
}
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
};
|
|
var TimelineSettingTab = class extends import_obsidian6.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
__publicField(this, "plugin");
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.resetSettings).setDesc(this.plugin.i18n.settings.resetSettingsDesc).addButton((button) => button.setButtonText(this.plugin.i18n.settings.resetSettingsButton).onClick(async () => {
|
|
this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS);
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.language).setDesc(this.plugin.i18n.settings.languageDesc).addDropdown((dropdown) => dropdown.addOption("zh-CN", "\u4E2D\u6587").addOption("en-US", "English").setValue(this.plugin.settings.language).onChange(async (value) => {
|
|
this.plugin.settings.language = value;
|
|
this.plugin.i18n = value === "zh-CN" ? zhCN : enUS;
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.milestoneAttribute).setDesc(this.plugin.i18n.settings.milestoneAttributeDesc).addText((text) => text.setPlaceholder("milestone").setValue(this.plugin.settings.milestoneAttribute).onChange(async (value) => {
|
|
this.plugin.settings.milestoneAttribute = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.milestoneValue).setDesc(this.plugin.i18n.settings.milestoneValueDesc).addText((text) => text.setPlaceholder("true or 1 (leave empty to match any value)").setValue(this.plugin.settings.milestoneValue).onChange(async (value) => {
|
|
this.plugin.settings.milestoneValue = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.lineWidth).setDesc(this.plugin.i18n.settings.lineWidthDesc).addSlider((slider) => slider.setLimits(1, 10, 1).setValue(this.plugin.settings.lineWidth).onChange(async (value) => {
|
|
this.plugin.settings.lineWidth = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.lineColor).setDesc(this.plugin.i18n.settings.lineColorDesc).addColorPicker((color) => color.setValue(this.plugin.settings.lineColor).onChange(async (value) => {
|
|
this.plugin.settings.lineColor = value;
|
|
await this.plugin.saveSettings();
|
|
})).addText((text) => text.setPlaceholder(this.plugin.i18n.settings.colorPickerPlaceholder).setValue("").onChange(async (value) => {
|
|
if (value) {
|
|
this.plugin.settings.lineColor = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.itemSpacing).setDesc(this.plugin.i18n.settings.itemSpacingDesc).addSlider((slider) => slider.setLimits(10, 100, 5).setValue(this.plugin.settings.itemSpacing).onChange(async (value) => {
|
|
this.plugin.settings.itemSpacing = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.cardBackground).setDesc(this.plugin.i18n.settings.cardBackgroundDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.cardBackground).onChange(async (value) => {
|
|
this.plugin.settings.cardBackground = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
).setTooltip(this.plugin.i18n.settings.colorPickerPlaceholder);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.cardTextColor).setDesc(this.plugin.i18n.settings.cardTextColorDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.cardTextColor).onChange(async (value) => {
|
|
this.plugin.settings.cardTextColor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.cardBorderColor).setDesc(this.plugin.i18n.settings.cardBorderColorDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.cardBorderColor).onChange(async (value) => {
|
|
this.plugin.settings.cardBorderColor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.milestoneCardBackground).setDesc(this.plugin.i18n.settings.milestoneCardBackgroundDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.milestoneCardBackground).onChange(async (value) => {
|
|
this.plugin.settings.milestoneCardBackground = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.milestoneCardTextColor).setDesc(this.plugin.i18n.settings.milestoneCardTextColorDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.milestoneCardTextColor).onChange(async (value) => {
|
|
this.plugin.settings.milestoneCardTextColor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.milestoneCardBorderColor).setDesc(this.plugin.i18n.settings.milestoneCardBorderColorDesc).addColorPicker(
|
|
(color) => color.setValue(this.plugin.settings.milestoneCardBorderColor).onChange(async (value) => {
|
|
this.plugin.settings.milestoneCardBorderColor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.animationDuration).setDesc(this.plugin.i18n.settings.animationDurationDesc).addSlider((slider) => slider.setLimits(0, 1e3, 50).setValue(this.plugin.settings.animationDuration).onChange(async (value) => {
|
|
this.plugin.settings.animationDuration = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.dateAttribute).setDesc(this.plugin.i18n.settings.dateAttributeDesc).addText((text) => text.setPlaceholder("created").setValue(this.plugin.settings.dateAttribute).onChange(async (value) => {
|
|
this.plugin.settings.dateAttribute = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.fileNamePrefix).setDesc(this.plugin.i18n.settings.fileNamePrefixDesc).addText((text) => text.setPlaceholder("").setValue(this.plugin.settings.fileNamePrefix).onChange(async (value) => {
|
|
this.plugin.settings.fileNamePrefix = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.fileNameSuffix).setDesc(this.plugin.i18n.settings.fileNameSuffixDesc).addText((text) => text.setPlaceholder("").setValue(this.plugin.settings.fileNameSuffix).onChange(async (value) => {
|
|
this.plugin.settings.fileNameSuffix = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian6.Setting(containerEl).setName(this.plugin.i18n.settings.defaultPosition).setDesc(this.plugin.i18n.settings.defaultPositionDesc).addDropdown(
|
|
(dropdown) => dropdown.addOption("left", this.plugin.i18n.settings.leftSidebar).addOption("right", this.plugin.i18n.settings.rightSidebar).setValue(this.plugin.settings.defaultPosition).onChange(async (value) => {
|
|
this.plugin.settings.defaultPosition = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
/* nosourcemap */ |