mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5906a1cf | ||
|
|
dc541dea2a | ||
|
|
eb908cb689 | ||
|
|
967ff18d8d | ||
|
|
89298b9652 | ||
|
|
e1694f298b | ||
|
|
61961f0c1d |
@@ -1175,6 +1175,15 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ChThH",
|
||||
"name": "CT Hall",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9499483?v=4",
|
||||
"profile": "https://github.com/ChThH",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -32,6 +32,7 @@ All the following commands are to be executed from the `packages/foam-vscode` di
|
||||
|
||||
Unit tests run in Node.js environment using Jest
|
||||
Integration tests require VS Code extension host
|
||||
When running tests, do not provide additional parameters, they are ignored by the custom runner script. You cannot run just a test, you have to run the whole suite.
|
||||
|
||||
Unit tests are named `*.test.ts` and integration tests are `*.spec.ts`. These test files live alongside the code in the `src` directory. An integration test is one that has a direct or indirect dependency on `vscode` module.
|
||||
There is a mock `vscode` module that can be used to run most integration tests without starting VS Code. Tests that can use this mock are start with the line `/* @unit-ready */`.
|
||||
|
||||
@@ -278,6 +278,7 @@ Foam is an evolving project and we welcome contributions:
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s-jacob-powell"><img src="https://avatars.githubusercontent.com/u/109111499?v=4?s=60" width="60px;" alt="S. Jacob Powell"/><br /><sub><b>S. Jacob Powell</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=s-jacob-powell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/figdavi"><img src="https://avatars.githubusercontent.com/u/99026991?v=4?s=60" width="60px;" alt="Davi Figueiredo"/><br /><sub><b>Davi Figueiredo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=figdavi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChThH"><img src="https://avatars.githubusercontent.com/u/9499483?v=4?s=60" width="60px;" alt="CT Hall"/><br /><sub><b>CT Hall</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ChThH" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.28.0"
|
||||
"version": "0.28.1"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [0.28.1] - 2025-09-25
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Removed duplicate links in dataviz graph (#1511 - thanks @Tenormis)
|
||||
- Use letter case to further disambiguate note identifiers (#1519, #1303)
|
||||
- Sanitize `filepath` before creating note from template (#1520, #1216)
|
||||
|
||||
## [0.28.0] - 2025-09-24
|
||||
|
||||
Features:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.28.0",
|
||||
"version": "0.28.1",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
|
||||
@@ -183,4 +183,41 @@ describe('Identifier computation', () => {
|
||||
workspace.getIdentifier(noteABis.uri, [noteB.uri, noteA.uri])
|
||||
).toEqual('note-a');
|
||||
});
|
||||
|
||||
it('should handle case-sensitive filenames correctly (#1303)', () => {
|
||||
const workspace = new FoamWorkspace('.md');
|
||||
const noteUppercase = createTestNote({ uri: '/a/Note.md' });
|
||||
const noteLowercase = createTestNote({ uri: '/b/note.md' });
|
||||
|
||||
workspace.set(noteUppercase).set(noteLowercase);
|
||||
|
||||
// Should find exact case matches
|
||||
expect(workspace.listByIdentifier('Note').length).toEqual(1);
|
||||
expect(workspace.listByIdentifier('Note')[0].uri.path).toEqual(
|
||||
'/a/Note.md'
|
||||
);
|
||||
|
||||
expect(workspace.listByIdentifier('note').length).toEqual(1);
|
||||
expect(workspace.listByIdentifier('note')[0].uri.path).toEqual(
|
||||
'/b/note.md'
|
||||
);
|
||||
|
||||
// Should not treat them as the same identifier
|
||||
expect(workspace.listByIdentifier('Note')[0]).not.toEqual(
|
||||
workspace.listByIdentifier('note')[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate correct identifiers for case-sensitive files', () => {
|
||||
const workspace = new FoamWorkspace('.md');
|
||||
const noteUppercase = createTestNote({ uri: '/a/Note.md' });
|
||||
const noteLowercase = createTestNote({ uri: '/b/note.md' });
|
||||
|
||||
workspace.set(noteUppercase).set(noteLowercase);
|
||||
|
||||
// Each should have a unique identifier without directory disambiguation
|
||||
// since they differ by case, they are not considered conflicting
|
||||
expect(workspace.getIdentifier(noteUppercase.uri)).toEqual('Note');
|
||||
expect(workspace.getIdentifier(noteLowercase.uri)).toEqual('note');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,13 +89,12 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
let needle = this.getTrieIdentifier(identifier);
|
||||
|
||||
const mdNeedle =
|
||||
getExtension(normalize(identifier)) !== this.defaultExtension
|
||||
? this.getTrieIdentifier(identifier + this.defaultExtension)
|
||||
: undefined;
|
||||
|
||||
const resources: Resource[] = [];
|
||||
let resources: Resource[] = [];
|
||||
|
||||
this._resources.find(needle).forEach(elm => resources.push(elm[1]));
|
||||
|
||||
@@ -103,6 +102,15 @@ export class FoamWorkspace implements IDisposable {
|
||||
this._resources.find(mdNeedle).forEach(elm => resources.push(elm[1]));
|
||||
}
|
||||
|
||||
// if multiple resources found, try to filter exact case matches
|
||||
if (resources.length > 1) {
|
||||
resources = resources.filter(
|
||||
r =>
|
||||
r.uri.getBasename() === identifier ||
|
||||
r.uri.getBasename() === identifier + this.defaultExtension
|
||||
);
|
||||
}
|
||||
|
||||
return resources.sort(Resource.sortByPath);
|
||||
}
|
||||
|
||||
@@ -115,7 +123,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
const amongst = [];
|
||||
const basename = forResource.getBasename();
|
||||
|
||||
this.listByIdentifier(basename).map(res => {
|
||||
this.listByIdentifier(basename).forEach(res => {
|
||||
// skip self
|
||||
if (res.uri.isEqual(forResource)) {
|
||||
return;
|
||||
|
||||
@@ -474,4 +474,132 @@ Content without filepath metadata.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filepath sanitization', () => {
|
||||
it('should sanitize invalid characters in filepath from template', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `---
|
||||
foam_template:
|
||||
filepath: \${FOAM_TITLE}.md
|
||||
---
|
||||
# \${FOAM_TITLE}`,
|
||||
metadata: new Map(),
|
||||
};
|
||||
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Title with many invalid characters (excluding / which is preserved for directories): \#%&{}<>?*$!'":@+`|=
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'Test\\#%&{}<>?*$!\'"Title:@+`|=');
|
||||
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// All invalid characters should become dashes: Test + 14 invalid chars + Title + : + @+`|= (6 more total)
|
||||
expect(result.filepath.path).toBe('Test--------------Title------.md');
|
||||
|
||||
// Content should remain unchanged
|
||||
expect(result.content).toContain('# Test\\#%&{}<>?*$!\'"Title:@+`|=');
|
||||
});
|
||||
|
||||
it('should not affect FOAM_TITLE when not used in filepath', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Template with static filepath, FOAM_TITLE only in content
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `---
|
||||
foam_template:
|
||||
filepath: notes/static-file.md
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
Content with \${FOAM_TITLE} should remain unchanged.`,
|
||||
metadata: new Map(),
|
||||
};
|
||||
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'Invalid "Characters" <Test>');
|
||||
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// Filepath should remain static (no sanitization needed)
|
||||
expect(result.filepath.path).toBe('notes/static-file.md');
|
||||
|
||||
// Content should use original FOAM_TITLE with invalid characters
|
||||
expect(result.content).toContain('# Invalid "Characters" <Test>');
|
||||
expect(result.content).toContain(
|
||||
'Content with Invalid "Characters" <Test> should remain'
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize complex filepath patterns with multiple variables', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `---
|
||||
foam_template:
|
||||
filepath: \${FOAM_DATE_YEAR}/\${FOAM_DATE_MONTH}/\${FOAM_TITLE}.md
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
Date and title combination.`,
|
||||
metadata: new Map(),
|
||||
};
|
||||
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
const testDate = new Date('2024-03-15');
|
||||
const resolver = new Resolver(new Map(), testDate);
|
||||
resolver.define('FOAM_TITLE', 'Note:With|Invalid*Chars');
|
||||
resolver.define('FOAM_DATE_YEAR', '2024');
|
||||
resolver.define('FOAM_DATE_MONTH', '03');
|
||||
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// Entire resolved filepath should be sanitized
|
||||
expect(result.filepath.path).toBe('2024/03/Note-With-Invalid-Chars.md');
|
||||
|
||||
// Content should use original FOAM_TITLE
|
||||
expect(result.content).toContain('# Note:With|Invalid*Chars');
|
||||
});
|
||||
|
||||
it('should handle filepath with no invalid characters', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `---
|
||||
foam_template:
|
||||
filepath: notes/\${FOAM_TITLE}.md
|
||||
---
|
||||
# \${FOAM_TITLE}`,
|
||||
metadata: new Map(),
|
||||
};
|
||||
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'ValidTitle123');
|
||||
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// No sanitization needed - should remain unchanged
|
||||
expect(result.filepath.path).toBe('notes/ValidTitle123.md');
|
||||
expect(result.content).toContain('# ValidTitle123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,26 @@ import {
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
/**
|
||||
* Characters that are invalid in file names
|
||||
* Based on UNALLOWED_CHARS from variable-resolver.ts but excluding forward slash
|
||||
* which is needed for directory separators in filepaths
|
||||
*/
|
||||
const FILEPATH_UNALLOWED_CHARS = '\\#%&{}<>?*$!\'":@+`|=';
|
||||
|
||||
/**
|
||||
* Sanitizes a filepath by replacing invalid characters with dashes
|
||||
* Note: Forward slashes (/) are preserved for directory separators
|
||||
* @param filepath The filepath to sanitize
|
||||
* @returns The sanitized filepath
|
||||
*/
|
||||
function sanitizeFilepath(filepath: string): string {
|
||||
// Escape special regex characters and create character class
|
||||
const escapedChars = FILEPATH_UNALLOWED_CHARS.replace(/[\\^\-\]]/g, '\\$&');
|
||||
const regex = new RegExp(`[${escapedChars}]`, 'g');
|
||||
return filepath.replace(regex, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified engine for creating notes from both Markdown and JavaScript templates
|
||||
*/
|
||||
@@ -109,10 +129,13 @@ export class NoteCreationEngine {
|
||||
]);
|
||||
|
||||
// Determine filepath - get variables from resolver for default generation
|
||||
const filepath =
|
||||
let filepath =
|
||||
metadata.get('filepath') ??
|
||||
(await this.generateDefaultFilepath(resolver));
|
||||
|
||||
// Sanitize the filepath to remove invalid characters
|
||||
filepath = sanitizeFilepath(filepath);
|
||||
|
||||
return {
|
||||
filepath: this.roots[0].forPath(filepath),
|
||||
content: cleanContent,
|
||||
|
||||
@@ -291,6 +291,19 @@ function augmentGraphInfo(graph) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const seen = new Set();
|
||||
graph.links = graph.links.filter(link => {
|
||||
const sourceId = getLinkNodeId(link.source);
|
||||
const targetId = getLinkNodeId(link.target);
|
||||
const key = `${sourceId} -> ${targetId}`;
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
graph.links.forEach(link => {
|
||||
const a = graph.nodeInfo[link.source];
|
||||
const b = graph.nodeInfo[link.target];
|
||||
@@ -299,6 +312,7 @@ function augmentGraphInfo(graph) {
|
||||
a.links.push(link);
|
||||
b.links.push(link);
|
||||
});
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
@@ -372,6 +372,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s-jacob-powell"><img src="https://avatars.githubusercontent.com/u/109111499?v=4?s=60" width="60px;" alt="S. Jacob Powell"/><br /><sub><b>S. Jacob Powell</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=s-jacob-powell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/figdavi"><img src="https://avatars.githubusercontent.com/u/99026991?v=4?s=60" width="60px;" alt="Davi Figueiredo"/><br /><sub><b>Davi Figueiredo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=figdavi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChThH"><img src="https://avatars.githubusercontent.com/u/9499483?v=4?s=60" width="60px;" alt="CT Hall"/><br /><sub><b>CT Hall</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ChThH" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user