Compare commits

...

7 Commits

Author SHA1 Message Date
Riccardo Ferretti
3b5906a1cf v0.28.1 2025-09-25 23:32:29 +02:00
Riccardo Ferretti
dc541dea2a Preparation for next release 2025-09-25 23:32:10 +02:00
Riccardo Ferretti
eb908cb689 added test instructions to CLAUDE 2025-09-25 23:27:57 +02:00
Riccardo
967ff18d8d Sanitize filepath in template before note creation (#1520)
fixes #1216
2025-09-25 17:42:44 +02:00
Riccardo
89298b9652 Use identifier case to further disambiguate notes (#1519)
Fixes #1303
2025-09-25 17:29:42 +02:00
Tenormis
e1694f298b Remove duplicate links between nodes (#1511)
Co-authored-by: tenormis <tenormis@mars.com>
2025-09-25 13:02:24 +02:00
allcontributors[bot]
61961f0c1d add ChThH as a contributor for code (#1515)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-09-24 12:30:44 +02:00
12 changed files with 237 additions and 7 deletions

View File

@@ -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,

View File

@@ -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 */`.

View File

@@ -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>

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.28.0"
"version": "0.28.1"
}

View File

@@ -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:

View File

@@ -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": {

View File

@@ -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');
});
});

View File

@@ -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;

View File

@@ -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');
});
});
});

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 -->
[![All Contributors](https://img.shields.io/badge/all_contributors-128-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-129-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode?label=VS%20Code%20Installs)](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>