mirror of
https://github.com/davidingerslev/outlook-meeting-notes.git
synced 2026-07-22 05:41:46 +00:00
Add existing project files to Github.
This commit is contained in:
commit
b401aa27b3
16 changed files with 3073 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
5
LICENSE
Normal file
5
LICENSE
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Copyright (C) 2020-2025 by Dynalist Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
125
README.md
Normal file
125
README.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Outlook Meeting Notes
|
||||
|
||||
This is a plugin for [Obsidian](https://obsidian.md) that allows you to create notes from
|
||||
Microsoft Outlook meetings, including the meeting details (date/time,
|
||||
subject, recipients, invite message, etc), using a customisable template.
|
||||
The plugin processes .msg files that are dragged-and-dropped from the
|
||||
Outlook desktop app onto the plugin icon in the ribbon in Obsidian
|
||||
|
||||
This makes it easy to take notes of meetings in Obsidian.
|
||||
|
||||
By processing .msg files, the plugin does not depend on having to run code within
|
||||
Outlook or on Microsoft 365 administrators authorising an app to connect to the
|
||||
Microsoft Graph API.
|
||||
|
||||
It relies on the wonderful [msgreader](https://github.com/HiraokaHyperTools/msgreader),
|
||||
[mustache.js](https://github.com/janl/mustache.js), and
|
||||
[mustache-validator](https://github.com/eliasm307/mustache-validator) libraries.
|
||||
|
||||
## Installation
|
||||
|
||||
Search for Outlook Meeting Notes in Community Plugins, either in the Obsidian app or
|
||||
on the [Obsidian website](https://obsidian.md/plugins?search=outlook+meeting+notes).
|
||||
|
||||
## Usage
|
||||
Drag and drop a meeting from a calendar in the Outlook Classic desktop app onto the
|
||||
plugin ribbon icon in Obsidian. The plugin will create a note and open it in the
|
||||
current tab.
|
||||
|
||||
Outlook Classic creates a .msg file when you drag and drop an appointment or meeting.
|
||||
You can also save a meeting as a .msg file (or save one you have received as an email
|
||||
attachment) and drag and drop the file onto the plugin icon.
|
||||
|
||||
The plugin includes a default template that adds the meeting details
|
||||
into the frontmatter (properties) of the created note. It sets the filename of the note
|
||||
to the date, time and subject of the meeting, and creates the note in the root folder of
|
||||
the vault. This can all be customised: see [Settings](#settings).
|
||||
|
||||
## Settings
|
||||
### Folder location
|
||||
The folder in which new notes will be created. If the folder doesn't already exist, it
|
||||
will be created (including any parent folders, if you set it as a subfolder like `a/b/c`)
|
||||
|
||||
### Filename pattern
|
||||
The pattern that will be used to create filenames, using [mustache syntax](https://mustache.github.io/mustache.5.html).
|
||||
The default pattern is:
|
||||
```
|
||||
{{#helper_dateFormat}}{{apptStartWhole}}|YYYY-MM-DD HH.mm{{/helper_dateFormat}} {{subject}}
|
||||
```
|
||||
This will produce filenames like `2025-03-09 10.31 Discuss documentation`.
|
||||
|
||||
The filename pattern can be used to create folders by using the `/` character. For example:
|
||||
```
|
||||
{{#helper_dateFormat}}{{apptStartWhole}}|YYYY/MM MMMM/YYYY-MM-DD HH.mm{{/helper_dateFormat}} {{subject}}
|
||||
```
|
||||
This would create a folder for the year, a sub-folder for the month and then a file in that sub-folder:
|
||||
`2025/03 March/2025-03-09 10.31 Discuss documentation`
|
||||
|
||||
If `/` characters appear in any of the fields from the Outlook meeting, they are replaced.
|
||||
|
||||
### Invalid character substitute
|
||||
When using Outlook fields as part of the filename, they may contain invalid characters.
|
||||
This setting allows you to specify what invalid characters should be replaced with. If
|
||||
blank, then invalid characters will be removed. If you specify a space, then any spaces
|
||||
at the end of the filename will be removed.
|
||||
|
||||
Note - the invalid characters are: `/` `*` `"` `\` `<` `>` `:` `|` `?`
|
||||
|
||||
### Template
|
||||
The default template can be customised, or you can write a new
|
||||
template using mustache syntax (see [the manual](https://mustache.github.io/mustache.5.html)).
|
||||
All .msg [fields](https://hiraokahypertools.github.io/msgreader/typedoc/interfaces/MsgReader.FieldsData.html)
|
||||
can be used in a template, and there are also some additional helper functions you can
|
||||
use to format fields.
|
||||
|
||||
### Default template
|
||||
The default template for notes is:
|
||||
```
|
||||
---
|
||||
title: {{subject}}
|
||||
subtitle: meeting notes
|
||||
date: {{#helper_dateFormat}}{{apptStartWhole}}|L LT{{/helper_dateFormat}}
|
||||
meeting: 'true'
|
||||
meeting-location: {{apptLocation}}
|
||||
meeting-recipients:
|
||||
{{#recipients}}
|
||||
- {{name}}
|
||||
{{/recipients}}
|
||||
meeting-invite: {{body}}
|
||||
---
|
||||
```
|
||||
|
||||
### Helper functions for templates
|
||||
To use a helper function in a template, include a section for the function, e.g.:
|
||||
```
|
||||
{{#helper_firstWord}}{{name}}{{/helper_firstWord}}
|
||||
```
|
||||
|
||||
#### helper_dateFormat
|
||||
Formats a date, using [moment.js](https://momentjs.com/). The format is delimited by a `|`
|
||||
character.
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{#helper_dateFormat}}{{apptStartWhole}}|YYYY-MM-DD HH.mm{{/helper_dateFormat}}
|
||||
```
|
||||
This is like *2025-03-09 16.14*.
|
||||
```
|
||||
{{#helper_dateFormat}}{{apptStartWhole}}|L LT{{/helper_dateFormat}}
|
||||
```
|
||||
A date in short format with a time in short format, using the Obsidian application's
|
||||
display language as a basis for the locale to use for the date format (you can change
|
||||
this setting in Settings > General > Language).
|
||||
|
||||
In *English (GB)* display language, this is like *09/03/2025 03:19*, but in
|
||||
*English* display language (which means US), it would be like *03/09/2025 3:19 AM*.
|
||||
|
||||
#### helper_firstWord
|
||||
Returns just the first word. This allows the option of recording just the first names of
|
||||
people who were invited to a meeting:
|
||||
```
|
||||
meeting-recipients:
|
||||
{{#recipients}}
|
||||
- {{#helper_firstWord}}{{name}}{{/helper_firstWord}}
|
||||
{{/recipients}}
|
||||
```
|
||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
464
main.ts
Normal file
464
main.ts
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
import { App, displayTooltip, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, moment } from 'obsidian';
|
||||
import MsgReader from '@kenjiuno/msgreader';
|
||||
import proxyData from 'mustache-validator';
|
||||
const Mustache = require('mustache');
|
||||
|
||||
const OutlookMeetingNotesDefaultFilenamePattern =
|
||||
'{{#helper_dateFormat}}{{apptStartWhole}}|YYYY-MM-DD HH.mm{{/helper_dateFormat}} {{subject}}';
|
||||
|
||||
const OutlookMeetingNotesDefaultTemplate = `---
|
||||
title: {{subject}}
|
||||
subtitle: meeting notes
|
||||
date: {{#helper_dateFormat}}{{apptStartWhole}}|L LT{{/helper_dateFormat}}
|
||||
meeting: 'true'
|
||||
meeting-location: {{apptLocation}}
|
||||
meeting-recipients:
|
||||
{{#recipients}}
|
||||
- {{name}}
|
||||
{{/recipients}}
|
||||
meeting-invite: {{body}}
|
||||
---
|
||||
`;
|
||||
|
||||
interface OutlookMeetingNotesSettings {
|
||||
notesFolder: string;
|
||||
invalidFilenameCharReplacement: string;
|
||||
fileNamePattern: string;
|
||||
notesTemplate: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: OutlookMeetingNotesSettings = {
|
||||
notesFolder: '',
|
||||
invalidFilenameCharReplacement: '',
|
||||
fileNamePattern: OutlookMeetingNotesDefaultFilenamePattern,
|
||||
notesTemplate: OutlookMeetingNotesDefaultTemplate
|
||||
}
|
||||
|
||||
export default class OutlookMeetingNotes extends Plugin {
|
||||
settings: OutlookMeetingNotesSettings;
|
||||
|
||||
//TODO: Add functionality to export meeting notes nicely
|
||||
|
||||
async createMeetingNote(msgPromise: Promise<MsgReader>) {
|
||||
const msg = await msgPromise;
|
||||
try {
|
||||
const { vault } = this.app;
|
||||
|
||||
// Get the file data from MsgReader
|
||||
const fileData = msg.getFileData();
|
||||
|
||||
// Check if we got a suitable meeting
|
||||
if (fileData.dataType != 'msg') {
|
||||
throw new TypeError('Outlook Meeting Notes cannot process the file. '
|
||||
+ 'MsgReader did not parse the file as valid msg format.');
|
||||
} else if (fileData.messageClass != 'IPM.Appointment') {
|
||||
throw new TypeError('Outlook Meeting Notes cannot process the file. '
|
||||
+ 'It is a valid msg file but not an appointment or meeting.');
|
||||
}
|
||||
|
||||
this.addHelperFunctions(fileData);
|
||||
|
||||
let folderPath = this.settings.notesFolder;
|
||||
if (folderPath == '') { folderPath = '/'; }
|
||||
const fileNameEscape = {
|
||||
escape: (str: string): string => {
|
||||
return str.replaceAll('/', this.settings.invalidFilenameCharReplacement);
|
||||
}
|
||||
}
|
||||
const fileNameMustache = Mustache.render(
|
||||
this.settings.fileNamePattern,
|
||||
proxyData(fileData),
|
||||
undefined,
|
||||
fileNameEscape)
|
||||
.replaceAll(/[*"\\<>:|?]/g, this.settings.invalidFilenameCharReplacement);
|
||||
const filePath = folderPath + '/' + fileNameMustache + '.md';
|
||||
const newFolderPath = filePath.replace(/\/[^/]*$/, '');
|
||||
let meetingNoteFile = vault.getFileByPath(filePath);
|
||||
if (meetingNoteFile) {
|
||||
// File already exists
|
||||
// TODO: send a message to the user
|
||||
}
|
||||
else {
|
||||
if (vault.getFolderByPath(newFolderPath) == null) {
|
||||
vault.createFolder(newFolderPath);
|
||||
}
|
||||
const mustacheOutput = this.renderTemplate(
|
||||
this.settings.notesTemplate,
|
||||
fileData);
|
||||
meetingNoteFile = await vault.create(filePath, mustacheOutput);
|
||||
new Notice('New file: ' + meetingNoteFile.basename);
|
||||
}
|
||||
const openInNewTab = false;
|
||||
this.app.workspace.getLeaf(openInNewTab).openFile(meetingNoteFile);
|
||||
const fe = this.app.internalPlugins.getEnabledPluginById("file-explorer");
|
||||
if (fe) { fe.revealInFolder(meetingNoteFile); }
|
||||
} catch (ee: unknown) {
|
||||
// TODO: Handle errors reasonably -- differently between msg missing elements and errors creating file
|
||||
if (ee instanceof Error) { new Notice(ee.name + ':\n' + ee.message); }
|
||||
throw ee;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom funciton to handle a file being dropped onto the ribbon icon.
|
||||
async handleDropEvent(dropevt: DragEvent) {
|
||||
if (dropevt.dataTransfer == null) {
|
||||
throw new ReferenceError('Outlook Meeting Notes cannot handle the DragEvent. The event had a null '
|
||||
+ 'dataTransfer property, which should never happen when dispatched by the browser, according '
|
||||
+ 'to https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/dataTransfer');
|
||||
} else {
|
||||
const droppedFiles = dropevt.dataTransfer.files
|
||||
if (droppedFiles.length != 1) {
|
||||
new Notice('Outlook Meeting Notes can only handle one meeting being dropped onto the ribbon icon');
|
||||
}
|
||||
else {
|
||||
// One file was dropped, hand it over to createMeetingNote
|
||||
const droppedFile = droppedFiles[0];
|
||||
new Notice('Dropped: ' + droppedFile.name);
|
||||
|
||||
const fr = new FileReader();
|
||||
fr.onload = () => {
|
||||
if (fr.result == null) {
|
||||
throw new ReferenceError('Outlook Meeting Notes cannot handle the DragEvent. The FileReader had '
|
||||
+ 'a null result property, which should not be possible.');
|
||||
} else if (!(fr.result instanceof ArrayBuffer)) {
|
||||
throw new TypeError('Outlook Meeting Notes cannot handle the DragEvent. The FileReader result '
|
||||
+ 'property was not an ArrayBuffer, which should be impossible.');
|
||||
} else {
|
||||
// As readAsArrayBuffer is being used, below, fr.result will be an ArrayBuffer.
|
||||
const msgRdr = new MsgReader(fr.result);
|
||||
this.createMeetingNote(msgRdr);
|
||||
}
|
||||
}
|
||||
fr.readAsArrayBuffer(droppedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
const tooltipMessage = 'Outlook Meeting Notes: Drag and drop a meeting onto this icon from Outlook (or a .msg file) to create a meeting note.';
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
// Create an icon that does nothing when clicked, as the effect is from
|
||||
const ribbonIconEl = this.addRibbonIcon('calendar-clock', tooltipMessage, () => { });
|
||||
|
||||
// These respond to something being dragged over the ribbon icon and dropped onto it
|
||||
this.registerEvent(ribbonIconEl.on('dragenter', 'div', () => {
|
||||
// Style for icon-dragover is defined in the CSS file.
|
||||
ribbonIconEl.toggleClass('is-being-dragged-over', true);
|
||||
// Display a tooltip.
|
||||
const ttPosition = ribbonIconEl.getAttribute('data-tooltip-position');
|
||||
const ttDelay = ribbonIconEl.getAttribute('data-tooltip-delay');
|
||||
if (ttPosition != null && ttDelay != null) {
|
||||
displayTooltip(ribbonIconEl, tooltipMessage, { placement: ttPosition, delay: Number(ttDelay) });
|
||||
} else {
|
||||
displayTooltip(ribbonIconEl, tooltipMessage);
|
||||
}
|
||||
}));
|
||||
this.registerEvent(ribbonIconEl.on('dragleave', 'div', () => {
|
||||
ribbonIconEl.toggleClass('is-being-dragged-over', false);
|
||||
// Remove the tooltip when the user leaves the ribbon icon while still dragging.
|
||||
const tooltip = document.getElementsByClassName('tooltip')[0]
|
||||
if (tooltip) { tooltip.remove(); }
|
||||
}));
|
||||
this.registerEvent(ribbonIconEl.on('dragover', 'div', (dragevt) => {
|
||||
// User is dragging something over the icon
|
||||
|
||||
// Prevent the default behaviour of not allowing the drop event
|
||||
dragevt.preventDefault();
|
||||
// The dropEffect changes the cursor. Options are 'none', 'move', 'copy', and 'link'.
|
||||
if (dragevt.dataTransfer != null) { dragevt.dataTransfer.dropEffect = 'copy'; }
|
||||
}));
|
||||
this.registerEvent(ribbonIconEl.on('drop', 'div', (dropevt) => {
|
||||
// User has dropped something on the icon
|
||||
|
||||
// If the user drops something, dragleave doesn't get called.
|
||||
ribbonIconEl.toggleClass('is-being-dragged-over', false);
|
||||
|
||||
// Prevent the default behaviour because we're handling it.
|
||||
dropevt.preventDefault();
|
||||
// Call the custom function defined above.
|
||||
this.handleDropEvent(dropevt);
|
||||
}));
|
||||
ribbonIconEl.addClass('outlook-meeting-notes-icon');
|
||||
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
// const statusBarItemEl = this.addStatusBarItem();
|
||||
// statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
//TODO: Add commands to create meeting notes.
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new OutlookMeetingNotesModal(this.app).open();
|
||||
}
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'OutlookMeetingNotes editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('OutlookMeetingNotes Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply 'checking' if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new OutlookMeetingNotesModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new OutlookMeetingNotesSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
// this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
// console.log('click', evt);
|
||||
// });
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
//this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
addHelperFunctions(hash: any): any {
|
||||
const helperFunctions = {
|
||||
firstWord: () => {
|
||||
return function (words: string, render: any) {
|
||||
return render(words).replace(/\W.*$/, '');
|
||||
}
|
||||
},
|
||||
dateFormat: () => {
|
||||
return function (datetime_format: string, render: any) {
|
||||
const parts = datetime_format.split('|')
|
||||
return moment(render(parts[0]).trim()).format(parts[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let func in helperFunctions) {
|
||||
hash['helper_' + func] = helperFunctions[func];
|
||||
}
|
||||
// Add helper functions to all objects in arrays so that the helper
|
||||
// functions work inside mustache sections
|
||||
for (let property in hash) {
|
||||
if (hash[property] instanceof Array) {
|
||||
for (let subproperty in hash[property]) {
|
||||
if (hash[property][subproperty] instanceof Object) {
|
||||
for (let func in helperFunctions) {
|
||||
hash[property][subproperty]['helper_' + func] = helperFunctions[func];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// renderTemplate() //
|
||||
//////////////////////
|
||||
// Parse template into YAML and markdown sections to use different escaping for each
|
||||
renderTemplate(template: string, hash: any): string {
|
||||
// Regex /^---(\r\n?|\n).*?(\r\n?|\n)---($|\r\n?|\n)/s matches '---' at the start of the string,
|
||||
// then a platform-independent new-line, followed by a non-greedy match (because of *?) of any
|
||||
// character including newlines (because of /s at end), followed by another --- on its own line
|
||||
// with either another platform-independent new-line afterwards or the end of the string.
|
||||
const templateYAMLMatch = template.match(/^---(\r\n?|\n).*?(\r\n?|\n)---($|\r\n?|\n)/s);
|
||||
// If there was a match, then make notesTemplateMD everything after it, otherwise it's the whole thing.
|
||||
const templateMD = templateYAMLMatch ? template.substring(templateYAMLMatch[0].length) : template;
|
||||
|
||||
let output = ''
|
||||
|
||||
if (templateYAMLMatch) {
|
||||
const mustacheYAMLOptions = {
|
||||
escape: (str: string): string => {
|
||||
// Escape YAML
|
||||
const found = str.match(/\r\n?|\n/);
|
||||
if (found) {
|
||||
return '|\n' + ' ' + str.replaceAll(/\r\n?|\n/g, '\n ');
|
||||
} else if (str.match(/[:#\[\]\{\},]/)) {
|
||||
return '"' + str.replaceAll(/["\\]/g, '\\$&') + '"';
|
||||
}
|
||||
else return str;
|
||||
}
|
||||
}
|
||||
|
||||
output = output + Mustache.render(
|
||||
templateYAMLMatch[0],
|
||||
proxyData(hash),
|
||||
undefined,
|
||||
mustacheYAMLOptions);
|
||||
}
|
||||
|
||||
if (templateMD) {
|
||||
const mustacheMDOptions = {
|
||||
escape: (str: string): string => {
|
||||
return str.replaceAll(/[\\\`\*\_\[\]\{\}\<\>\(\)\#\!\|\^]/g, '\\$&')
|
||||
.replaceAll('%%', '\\%\\%')
|
||||
.replaceAll('~~', '\\~\\~')
|
||||
.replaceAll('==', '\\=\\=');
|
||||
}
|
||||
}
|
||||
|
||||
output = output + Mustache.render(
|
||||
templateMD,
|
||||
proxyData(hash),
|
||||
undefined,
|
||||
mustacheMDOptions);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OutlookMeetingNotesModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class OutlookMeetingNotesSettingTab extends PluginSettingTab {
|
||||
plugin: OutlookMeetingNotes;
|
||||
|
||||
constructor(app: App, plugin: OutlookMeetingNotes) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Folder location')
|
||||
.setDesc('Notes will be created in this folder.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Example: folder 1/subfolder 2')
|
||||
.setValue(this.plugin.settings.notesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.notesFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Filename pattern')
|
||||
.setDesc('This pattern will be used to name new notes.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Default: ' + OutlookMeetingNotesDefaultFilenamePattern)
|
||||
.setValue(this.plugin.settings.fileNamePattern)
|
||||
.onChange(async (value) => {
|
||||
if (value == '') {
|
||||
this.plugin.settings.fileNamePattern = OutlookMeetingNotesDefaultFilenamePattern;
|
||||
} else {
|
||||
this.plugin.settings.fileNamePattern = value;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Invalid character substitute')
|
||||
.setDesc('This character (or string) will be used in place of any invalid characters for new note filenames.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Example: _')
|
||||
.setValue(this.plugin.settings.invalidFilenameCharReplacement)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.invalidFilenameCharReplacement = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Template')
|
||||
.setDesc('This template will be used for new notes.')
|
||||
.addTextArea(text => text
|
||||
.setPlaceholder('Default: ' + OutlookMeetingNotesDefaultFilenamePattern)
|
||||
.setValue(this.plugin.settings.notesTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.notesTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setDesc((() => {
|
||||
const df = document.createDocumentFragment();
|
||||
df.appendChild(document.createTextNode(
|
||||
'For more information about filename patterns and the syntax for templates, see the '
|
||||
));
|
||||
const link = document.createElement('a');
|
||||
link.href = 'https://www.github.com/davidingerslev/outlook-meeting-notes/#outlook-meeting-notes';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener';
|
||||
link.textContent = 'documentation';
|
||||
df.appendChild(link);
|
||||
df.appendChild(document.createTextNode('.'))
|
||||
return df;
|
||||
})())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Notes from the sample plugin README.md
|
||||
|
||||
## Releasing new releases
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
*/
|
||||
11
manifest.json
Normal file
11
manifest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "outlook-meeting-notes",
|
||||
"name": "Outlook Meeting Notes",
|
||||
"version": "0.0.2",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Creates meeting notes for Outlook appointments and meetings.",
|
||||
"author": "David Ingerslev",
|
||||
"authorUrl": "https://github.com/davidingerslev",
|
||||
"fundingUrl": "https://github.com/davidingerslev",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2278
package-lock.json
generated
Normal file
2278
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
package.json
Normal file
29
package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kenjiuno/msgreader": "^1.22.0",
|
||||
"mustache": "^4.2.0",
|
||||
"mustache-validator": "^0.2.0"
|
||||
}
|
||||
}
|
||||
12
styles.css
Normal file
12
styles.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
|
||||
This CSS file will be included with your plugin, and
|
||||
available in the app when your plugin is enabled.
|
||||
|
||||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
.outlook-meeting-notes-icon.is-being-dragged-over {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--interactive-accent-hover);
|
||||
}
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue