Skip to content

Commit 5a0e072

Browse files
committed
test implementation of #296
1 parent 164b4e1 commit 5a0e072

File tree

13 files changed

+383
-44
lines changed

13 files changed

+383
-44
lines changed

packages/core/src/api/FileAPI.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { LinePosition } from 'packages/core/src/config/APIConfigs';
12
import type { IPlugin } from 'packages/core/src/IPlugin';
23

34
export abstract class FileAPI<Plugin extends IPlugin> {
@@ -58,6 +59,24 @@ export abstract class FileAPI<Plugin extends IPlugin> {
5859
*/
5960
public abstract getPathByName(name: string, relativeTo?: string): string | undefined;
6061

62+
public getFrontmatterLocation(fileContent: string): LinePosition | undefined {
63+
const splitContent = fileContent.split('\n');
64+
if (splitContent.at(0) !== '---') {
65+
return undefined;
66+
}
67+
68+
for (let i = 1; i < splitContent.length; i++) {
69+
if (splitContent.at(i) === '---') {
70+
return {
71+
lineStart: 1,
72+
lineEnd: i + 1,
73+
};
74+
}
75+
}
76+
77+
return undefined;
78+
}
79+
6180
/**
6281
* Checks if a file path has been excluded in the settings.
6382
*

packages/core/src/config/ButtonConfig.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ export interface CreateNoteButtonAction {
9494

9595
export interface ReplaceInNoteButtonAction {
9696
type: ButtonActionType.REPLACE_IN_NOTE;
97-
fromLine: number;
98-
toLine: number;
97+
fromLine: number | string;
98+
toLine: number | string;
9999
replacement: string;
100100
templater?: boolean;
101101
}
@@ -115,7 +115,7 @@ export interface RegexpReplaceInNoteButtonAction {
115115

116116
export interface InsertIntoNoteButtonAction {
117117
type: ButtonActionType.INSERT_INTO_NOTE;
118-
line: number;
118+
line: number | string;
119119
value: string;
120120
templater?: boolean;
121121
}

packages/core/src/config/validators/ButtonConfigValidators.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ function actionFieldNumber(action: string, name: string, description: string) {
3232
function actionFieldString(action: string, name: string, description: string) {
3333
return z.string({
3434
required_error: `The ${action} action requires a specified ${description} with the '${name}' field.`,
35+
});
36+
}
37+
38+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
39+
function actionFieldCoerceString(action: string, name: string, description: string) {
40+
return z.coerce.string({
41+
required_error: `The ${action} action requires a specified ${description} with the '${name}' field.`,
3542
invalid_type_error: `The ${action} action requires the value of the '${name}' fields to be a string.`,
3643
});
3744
}
@@ -137,8 +144,8 @@ export const V_CreateNoteButtonAction = schemaForType<CreateNoteButtonAction>()(
137144
export const V_ReplaceInNoteButtonAction = schemaForType<ReplaceInNoteButtonAction>()(
138145
z.object({
139146
type: z.literal(ButtonActionType.REPLACE_IN_NOTE),
140-
fromLine: actionFieldNumber('replaceInNote', 'fromLine', 'line to replace from'),
141-
toLine: actionFieldNumber('replaceInNote', 'toLine', 'line to replace to'),
147+
fromLine: actionFieldCoerceString('replaceInNote', 'fromLine', 'line to replace from'),
148+
toLine: actionFieldCoerceString('replaceInNote', 'toLine', 'line to replace to'),
142149
replacement: actionFieldString('replaceInNote', 'replacement', 'replacement string'),
143150
templater: actionFieldBool('replaceInNote', 'templater', 'value for whether to use Templater').optional(),
144151
}),
@@ -168,7 +175,7 @@ export const V_RegexpReplaceInNoteButtonAction = schemaForType<RegexpReplaceInNo
168175
export const V_InsertIntoNoteButtonAction = schemaForType<InsertIntoNoteButtonAction>()(
169176
z.object({
170177
type: z.literal(ButtonActionType.INSERT_INTO_NOTE),
171-
line: actionFieldNumber('insertIntoNote', 'line', 'line to insert at'),
178+
line: actionFieldCoerceString('insertIntoNote', 'line', 'line to insert at'),
172179
value: actionFieldString('insertIntoNote', 'value', 'string to insert'),
173180
templater: actionFieldBool('insertIntoNote', 'templater', 'value for whether to use Templater').optional(),
174181
}),

packages/core/src/fields/button/ButtonActionRunner.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { LinePosition } from 'packages/core/src/config/APIConfigs';
12
import type {
23
ButtonActionMap,
34
ButtonClickContext,
@@ -24,6 +25,7 @@ import { UpdateMetadataButtonActionConfig } from 'packages/core/src/fields/butto
2425
import type { IPlugin } from 'packages/core/src/IPlugin';
2526
import { MDLinkParser } from 'packages/core/src/parsers/MarkdownLinkParser';
2627
import { ErrorLevel, MetaBindParsingError } from 'packages/core/src/utils/errors/MetaBindErrors';
28+
import type { LineNumberContext } from 'packages/core/src/utils/LineNumberExpression';
2729

2830
type ActionContexts = {
2931
[key in ButtonActionType]: AbstractButtonActionConfig<ButtonActionMap[key]>;
@@ -161,4 +163,21 @@ export class ButtonActionRunner {
161163
altKey: event.altKey,
162164
};
163165
}
166+
167+
getLineNumberContext(fileContent: string, selfNotePosition: LinePosition | undefined): LineNumberContext {
168+
const fileStart = 1;
169+
const fileEnd = fileContent.split('\n').length;
170+
const frontmatterPosition = this.plugin.internal.file.getFrontmatterLocation(fileContent);
171+
172+
return {
173+
fileStart: fileStart,
174+
fileEnd: fileEnd,
175+
frontmatterStart: frontmatterPosition ? frontmatterPosition.lineStart : fileStart,
176+
frontmatterEnd: frontmatterPosition ? frontmatterPosition.lineEnd : fileStart,
177+
contentStart: frontmatterPosition ? frontmatterPosition.lineEnd + 1 : fileStart,
178+
contentEnd: fileEnd,
179+
selfStart: selfNotePosition ? selfNotePosition.lineStart + 1 : undefined,
180+
selfEnd: selfNotePosition ? selfNotePosition.lineEnd + 1 : undefined,
181+
};
182+
}
164183
}

packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
import { ButtonActionType } from 'packages/core/src/config/ButtonConfig';
88
import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig';
99
import type { IPlugin } from 'packages/core/src/IPlugin';
10+
import { P_lineNumberExpression } from 'packages/core/src/parsers/nomParsers/MiscNomParsers';
11+
import { runParser } from 'packages/core/src/parsers/ParsingError';
1012

1113
export class InsertIntoNoteButtonActionConfig extends AbstractButtonActionConfig<InsertIntoNoteButtonAction> {
1214
constructor(plugin: IPlugin) {
@@ -17,7 +19,7 @@ export class InsertIntoNoteButtonActionConfig extends AbstractButtonActionConfig
1719
_config: ButtonConfig | undefined,
1820
action: InsertIntoNoteButtonAction,
1921
filePath: string,
20-
_context: ButtonContext,
22+
context: ButtonContext,
2123
_click: ButtonClickContext,
2224
): Promise<void> {
2325
const insertString = action.templater
@@ -27,17 +29,22 @@ export class InsertIntoNoteButtonActionConfig extends AbstractButtonActionConfig
2729
)
2830
: action.value;
2931

32+
const line = runParser(P_lineNumberExpression, action.line.toString());
33+
3034
await this.plugin.internal.file.atomicModify(filePath, content => {
3135
let splitContent = content.split('\n');
3236

33-
if (action.line < 1 || action.line > splitContent.length + 1) {
37+
const lineContext = this.plugin.api.buttonActionRunner.getLineNumberContext(content, context.position);
38+
const lineNumber = line.evaluate(lineContext);
39+
40+
if (lineNumber < 1 || lineNumber > splitContent.length) {
3441
throw new Error('Line number out of bounds');
3542
}
3643

3744
splitContent = [
38-
...splitContent.slice(0, action.line - 1),
45+
...splitContent.slice(0, lineNumber - 1),
3946
insertString,
40-
...splitContent.slice(action.line - 1),
47+
...splitContent.slice(lineNumber - 1),
4148
];
4249

4350
return splitContent.join('\n');

packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
import { ButtonActionType } from 'packages/core/src/config/ButtonConfig';
88
import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig';
99
import type { IPlugin } from 'packages/core/src/IPlugin';
10+
import { P_lineNumberExpression } from 'packages/core/src/parsers/nomParsers/MiscNomParsers';
11+
import { runParser } from 'packages/core/src/parsers/ParsingError';
1012

1113
export class ReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig<ReplaceInNoteButtonAction> {
1214
constructor(plugin: IPlugin) {
@@ -17,31 +19,41 @@ export class ReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig<
1719
_config: ButtonConfig | undefined,
1820
action: ReplaceInNoteButtonAction,
1921
filePath: string,
20-
_context: ButtonContext,
22+
context: ButtonContext,
2123
_click: ButtonClickContext,
2224
): Promise<void> {
23-
if (action.fromLine > action.toLine) {
24-
throw new Error('From line cannot be greater than to line');
25-
}
26-
2725
const replacement = action.templater
2826
? await this.plugin.internal.evaluateTemplaterTemplate(
2927
this.plugin.api.buttonActionRunner.resolveFilePath(action.replacement),
3028
filePath,
3129
)
3230
: action.replacement;
3331

32+
const fromLine = runParser(P_lineNumberExpression, action.fromLine.toString());
33+
const toLine = runParser(P_lineNumberExpression, action.toLine.toString());
34+
3435
await this.plugin.internal.file.atomicModify(filePath, content => {
3536
let splitContent = content.split('\n');
3637

37-
if (action.fromLine < 0 || action.toLine > splitContent.length + 1) {
38-
throw new Error('Line numbers out of bounds');
38+
const lineContext = this.plugin.api.buttonActionRunner.getLineNumberContext(content, context.position);
39+
const fromLineNumber = fromLine.evaluate(lineContext);
40+
const toLineNumber = toLine.evaluate(lineContext);
41+
42+
if (fromLineNumber > toLineNumber) {
43+
throw new Error(`From line (${fromLineNumber}) can't be greater than to line (${toLineNumber})`);
44+
}
45+
46+
if (fromLineNumber < 1) {
47+
throw new Error(`From line (${fromLineNumber}) can't smaller than 1.`);
48+
}
49+
if (toLineNumber > splitContent.length) {
50+
throw new Error(`To line (${toLineNumber}) can't greater than the file length ${splitContent.length}.`);
3951
}
4052

4153
splitContent = [
42-
...splitContent.slice(0, action.fromLine - 1),
54+
...splitContent.slice(0, fromLineNumber - 1),
4355
replacement,
44-
...splitContent.slice(action.toLine),
56+
...splitContent.slice(toLineNumber),
4557
];
4658

4759
return splitContent.join('\n');

packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class ReplaceSelfButtonActionConfig extends AbstractButtonActionConfig<Re
4444
await this.plugin.internal.file.atomicModify(filePath, content => {
4545
let splitContent = content.split('\n');
4646

47-
if (position.lineStart < 0 || position.lineEnd > splitContent.length + 1) {
47+
if (position.lineStart < 0 || position.lineEnd > splitContent.length) {
4848
throw new Error('Position of the button in the note is out of bounds');
4949
}
5050

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Parser } from '@lemons_dev/parsinom/lib/Parser';
2+
import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils';
3+
import { P } from '@lemons_dev/parsinom/lib/ParsiNOM';
4+
import { P_Ident } from 'packages/core/src/parsers/nomParsers/GeneralNomParsers';
5+
import { LineNumberExpression, lineNumberOpFromString } from 'packages/core/src/utils/LineNumberExpression';
6+
7+
export const P_float: Parser<number> = P.sequenceMap(
8+
(sign, number) => (sign === undefined ? number : -number),
9+
P.string('-').optional(),
10+
P.or(
11+
P.sequenceMap((a, b, c) => Number(a + b + c), P_UTILS.digits(), P.string('.'), P_UTILS.digits()),
12+
P_UTILS.digits().map(x => Number(x)),
13+
),
14+
).thenEof();
15+
16+
export const P_int: Parser<number> = P.sequenceMap(
17+
(sign, number) => (sign === undefined ? number : -number),
18+
P.string('-').optional(),
19+
P_UTILS.digits().map(x => Number(x)),
20+
).thenEof();
21+
22+
export const P_lineNumberExpression = P.or(
23+
P.sequenceMap(
24+
(ident, op, number) => new LineNumberExpression(ident, lineNumberOpFromString(op), number),
25+
P_Ident,
26+
P.or(P.string('+'), P.string('-')).trim(P_UTILS.optionalWhitespace()),
27+
P_int,
28+
),
29+
P_Ident.map(ident => new LineNumberExpression(ident, undefined, undefined)),
30+
P_int.map(number => new LineNumberExpression(undefined, undefined, number)),
31+
);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export enum LineNumberOp {
2+
ADD = '+',
3+
SUB = '-',
4+
}
5+
6+
export function lineNumberOpFromString(op: string): LineNumberOp {
7+
if (op === '+') {
8+
return LineNumberOp.ADD;
9+
} else if (op === '-') {
10+
return LineNumberOp.SUB;
11+
}
12+
throw new Error(`Invalid LineNumberOp: ${op}`);
13+
}
14+
15+
export function lineNumberOpToNumber(op: LineNumberOp | undefined): number {
16+
if (op === undefined) {
17+
return 1;
18+
} else if (op === LineNumberOp.ADD) {
19+
return 1;
20+
} else if (op === LineNumberOp.SUB) {
21+
return -1;
22+
} else {
23+
throw new Error(`Invalid LineNumberOp: ${op}`);
24+
}
25+
}
26+
27+
export interface LineNumberContext {
28+
/**
29+
* Start of the file, so 1.
30+
*/
31+
fileStart: number;
32+
/**
33+
* End of the file, so the number of lines in the file.
34+
*/
35+
fileEnd: number;
36+
/**
37+
* Start of the frontmatter, so 1.
38+
*/
39+
frontmatterStart: number;
40+
/**
41+
* End of the frontmatter, so the line number of the last line of the frontmatter (with the ending "---").
42+
*/
43+
frontmatterEnd: number;
44+
/**
45+
* The line after the frontmatter, so the first line of the content.
46+
*/
47+
contentStart: number;
48+
/**
49+
* The end of the content, so the last line of the content.
50+
*/
51+
contentEnd: number;
52+
/**
53+
* The start of the code block, so "```language".
54+
*/
55+
selfStart?: number;
56+
/**
57+
* The end of the code block, so "```".
58+
*/
59+
selfEnd?: number;
60+
}
61+
62+
export class LineNumberExpression {
63+
literal: string | undefined;
64+
op: LineNumberOp | undefined;
65+
number: number | undefined;
66+
67+
constructor(literal: string | undefined, op: LineNumberOp | undefined, number: number | undefined) {
68+
this.literal = literal;
69+
this.op = op;
70+
this.number = number;
71+
}
72+
73+
evaluate(context: LineNumberContext): number {
74+
const resolvedLiteral = this.resolveLiteral(context);
75+
const op = lineNumberOpToNumber(this.op);
76+
77+
if (resolvedLiteral !== undefined && this.number !== undefined) {
78+
return resolvedLiteral + this.number * op;
79+
}
80+
81+
if (this.number !== undefined) {
82+
return this.number * op;
83+
}
84+
85+
if (resolvedLiteral !== undefined) {
86+
return resolvedLiteral;
87+
}
88+
89+
return 0;
90+
}
91+
92+
resolveLiteral(context: LineNumberContext): number | undefined {
93+
if (this.literal === undefined) {
94+
return undefined;
95+
}
96+
97+
if (this.literal in context) {
98+
return (context as unknown as Record<string, number>)[this.literal];
99+
}
100+
101+
return undefined;
102+
}
103+
}

0 commit comments

Comments
 (0)