File indicators update

- File reading for `jira-sync-*` is now much more time efficient.
- Readmes updated in terms of how file body indicators work.
- Template example reworked.
- Fixed how hidden indicators look like in Live Preview (no more glitching lines)
This commit is contained in:
Alamion 2026-01-27 21:23:35 +03:00
parent a4ec70fecd
commit b43f731b0a
No known key found for this signature in database
GPG key ID: E5712F673595B31D
9 changed files with 315 additions and 130 deletions

View file

@ -17,16 +17,57 @@ Without a template, such a page will be completely empty except for its title, s
#### Template
The template can consist of several different parts:
1. **formatter** - meta-information at the top of the screen. When specifying keys for it and using any `Get issues from Jira` variant, they will be populated with corresponding fields from the response data.
2. **body** - the main content of the page. When using indicators like `jira-sync-section-*`, `jira-sync-line-*`, or `jira-sync-inline-start-*`, they will be filled with the corresponding fields from the response data. The difference between these options is as follows: `line` reads and writes values from the current line, separating the indicator and value with a space. `section` reads values from multiple lines after the indicator, stopping only at another indicator or a heading. `inline` allows indicators to be placed anywhere in the text, not strictly at the beginning of lines. All indicators are invisible until highlighted with the mouse, providing a cleaner editing experience.
##### Frontmatter
Meta-information at the top of the screen. When specifying keys for it and using any `Get issues from Jira` variant, they will be populated with corresponding fields from the response data.
##### Body
The main content of the page. When using indicators like `jira-sync-"type"-*`, they will be filled with the corresponding fields from the response data.
The difference between these options is as follows:
- `line` reads and writes values from the current line.
Example:
```md
The responsible person for this task is `jira-sync-line-assignee` Bob
The description is `jira-sync-line-description` Some description
```
- `section` reads values from multiple lines after the indicator, stopping only at any other indicator or a heading.
Example:
```md
### Responsible `jira-sync-section-assignee`
Bob
### Description `jira-sync-section-description`
Some description
`jira-sync-section-customfield_10842`
...
```
- `inline` allows indicator of start and end to be placed anywhere in the text. Must have an ending part of indicator (`jira-sync-end`).
Example:
```md
The responsible person for this task is `jira-sync-inline-start-assignee`Bob`jira-sync-end`, and the description is `jira-sync-inline-start-description`Some description`jira-sync-end`.
```
- `block` is basically the same as `inline`, but with line break before and after indicators.
Example:
```md
The responsible person for this task is `jira-sync-block-assignee`
Bob
`jira-sync-end`, and the description is `jira-sync-block-description`
Some description
`jira-sync-end`.
```
An example can be found in [[docs/template_example]].
It is highly recommended to specify basic values in the template's formatter: `key` - the Jira task ID used for updates, `summary` - the Jira task title, and `status` - the current status of the task in Jira.
It is highly recommended to specify the following basic values in the template's formatter: `key` - the Jira task ID used for updates, `summary` - the Jira task title, and `status` - the current status of the task in Jira.
The formatter takes priority and will overwrite file content values when updating a task in Jira if a field's value is present in both as formatter saves initial format of value and when putting it in body we parse it to string.
Not all fields are predefined, and some may need adjustments. For example, the template provided in [[docs/template_example]] will not correctly retrieve `progressPercent` and `creator` from Jira, even though these fields exist. To fix this, refer to the advanced usage section below.
Not all fields are predefined, and some may need adjustments. To add any custom nonusual fields, features of their representation in .md format and how to update their info in Jira, refer to the advanced usage section below.
### Commands

View file

@ -17,16 +17,57 @@
#### Шаблон
Шаблон можно составить из нескольких разных частей:
1. **formatter** - он же метаинформация сверху экрана. При указании ключей для него и любом варианте `Get issues from Jira` они будут заполняться соответствующими полями из ответных данных.
2. **body** - основное содержимое страницы. При написании указателя вида `jira-sync-section-*`, `jira-sync-line-*` или `jira-sync-inline-start-*` они будут заполняться соответствующими полями из ответных данных. Разница между этими вариантами в следующем: `line` читает и пишет значение из текущей строки, разделяя указатель и значение пробелом. `section` читает значение из ряда строк после указателя, останавливаясь только при другом указателе или заголовке. `inline` позволяет индикаторам располагаться где угодно, а не строго в начале строки. Все индикаторы являются невидимыми пока не выделишь их мышкой, обеспечивая более чистый опыт редактирования.
Пример можно посмотреть в [[docs/template_example]]
##### Frontmatter
Оно же метаинформация сверху экрана. При указании ключей для него и любом варианте `Get issues from Jira` они будут заполняться соответствующими полями из ответных данных.
Настоятельно рекомендуется указать в formatter шаблоона базовые значения: `key` - id задачи в Jira, по которому производится обновление, `summary` - название задачи в Jira, `status` - текущий статус задачи в Jira.
##### Body
Основное содержание страницы. При использовании индикаторов типа `jira-sync-"type"-*` они будут заполняться соответствующими полями из сырых данных задачи.
formatter имеет приоритет и будет перезаписывать значения body, если значения для одного поля имеются в обоих местах, так как formatter сохраняет изначальный тип переменной, а body конвертирует её в string.
Разница между этими параметрами заключается в следующем:
Не все поля прописаны заранее и некоторые могут нуждаться в доработке. Пример предоставленный в [[docs/template_example]] например, не сможет подтянуть корректно `progressPercent` и `creator` из Jira, хотя такие поля существуют. Чтобы это исправить, нужно отбратиться к разделу продвинутого использования ниже.
- `line` считывает и записывает значения из текущей строки.
Пример:
```md
Ответственным за эту задачу является `jira-sync-line-assignee` Боб.
Описание: `jira-sync-line-description` Некоторое описание.
```
- `section` считывает значения из нескольких строк после индикатора, останавливаясь только на любом другом индикаторе или заголовке.
Пример:
```md
### Ответственный `jira-sync-section-assignee`
Боб
### Описание `jira-sync-section-description`
Некоторое описание
`jira-sync-section-customfield_10842`...
```
- `inline` позволяет размещать индикатор начала и окончания в любом месте текста. Должна быть конечная часть индикатора (`jira-sync-end`).
Пример:
```md
Ответственным за эту задачу является `jira-sync-inline-start-assignee`Боб`jira-sync-end`, а описание — `jira-sync-inline-start-description`Некоторое описание`jira-sync-end`.
```
- `block` в основном аналогичен `inline`, но с разрывом строки перед и после индикаторов.
Пример:
```md
Ответственным за эту задачу является `jira-sync-block-assignee`
Bob
`jira-sync-end`, а описание — `jira-sync-block-description`
Некоторое описание
`jira-sync-end`.
```
Пример можно найти в [[docs/template_example]].
Настоятельно рекомендуется указать следующие базовые значения в форматере шаблона: `key` — ID задачи Jira, используемый для обновлений, `summary` — название задачи Jira и `status` — текущий статус задачи в Jira.
Форматировщик имеет приоритет и перезаписывает значения содержимого файла при обновлении задачи в Jira, если значение поля присутствует в обоих, поскольку форматировщик сохраняет исходный формат значения, а при помещении его в тело мы преобразуем его в строку.
Не все поля являются предопределенными, и некоторые из них могут потребовать дополнительной настройки. Чтобы добавить любые нестандартные поля, узнать об особенностях их представления в формате .md и о том, как обновлять их информацию в Jira, обратитесь к разделу "Продвинутое использование" ниже.
### Команды

View file

@ -1,21 +1,99 @@
---
key:
assignee:
summary:
status:
deadline:
link:
progress:
tags: []
key: JIR-1234
status: In Progress
priority: Medium
sprint: Mobile-Q2-24
epic: API-Overhaul
link: http://jira.local:8000/browse/JIR-1234
---
### Time spent
```timekeep
{"entries":[]}
```
# Main Task Description
### Deadline
`INPUT[datePicker:deadline]`
This is a comprehensive test file for the jira-sync parser.
### Description
`jira-sync-section-description`
## Summary `jira-sync-section-summary`
Complete API standardization and consolidation across all microservices to improve developer experience and reduce integration complexity.
This section continues here with more details.
Multiple paragraphs are supported.
## Details Section
### Customer Impact `jira-sync-section-customfield_10842`
The current API structure is too fragmented and requires standardization. Customers are experiencing difficulties integrating with our services due to:
- Inconsistent authentication methods
- Different response formats
- Lack of versioning strategy
This impacts approximately 2,300 enterprise customers.
### Next Heading Should Stop Section
This content should NOT be part of customfield_10842.
## Assignment Info
Primary assignee: `jira-sync-inline-start-assignee`Jack A.M.`jira-sync-end` with backup from `jira-sync-inline-start-assignee_backup`Sarah Chen`jira-sync-end`.
Reporter: `jira-sync-inline-start-reporter`Mike Johnson`jira-sync-end`
## Time Tracking
Expected time spent on the task: `jira-sync-line-originalEstimate`1w 3d
Actual time logged so far: `jira-sync-line-timeSpent`2d 4h
Remaining estimate: `jira-sync-line-remainingEstimate`5d
## Acceptance Criteria `jira-sync-section-customfield_10100`
1. All endpoints follow RESTful conventions
2. API versioning implemented via headers
3. Consistent error handling across services
4. OpenAPI 3.0 documentation published
5. Migration guide for existing integrations
All criteria must be met before moving to Done.
### Technical Requirements
This is a separate section that shouldn't be included above.
## Code Block Examples
Here's a configuration block:
`jira-sync-block-start-customfield_10500`
{
"apiVersion": "v2",
"authentication": "OAuth2",
"rateLimit": 1000
}
`jira-sync-end`
And another block for database config:
`jira-sync-block-start-customfield_10501`
host: prod-db-01.internal
port: 5432
database: api_gateway
ssl: true
`jira-sync-end`
## Edge Cases Testing
### Empty Inline
Status indicator: `jira-sync-inline-start-status_indicator``jira-sync-end`
### Whitespace Heavy Section `jira-sync-section-whitespace_test`
This section has leading and trailing whitespace
Should be trimmed properly before sending to Jira.
### Single Line Section `jira-sync-section-oneliner`
Just one line here.
## Multi-line Inline Test
Complex description: `jira-sync-inline-start-description` This is a description
that spans multiple lines
with various formatting `jira-sync-inline-end` and continues here.

View file

@ -1,7 +1,7 @@
{
"id": "jira-sync",
"name": "Jira Issue Manager",
"version": "1.4.0",
"version": "1.4.1",
"minAppVersion": "1.10.1",
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
"author": "Alamion",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "1.4.0",
"version": "1.4.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {

View file

@ -8,4 +8,13 @@ tags:
description:
---
### Time spend
\`\`\`timekeep
{"entries":[]}
\`\`\`
### Description
\`jira-sync-section-description\`
### Other info
`;

View file

@ -39,8 +39,8 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
for (const match of text.matchAll(/`(jira-sync-[^`]+)`/g)) {
const start = from + match.index!;
const end = start + match[0].length;
const contentStart = start + 1; // без первой `
const contentEnd = end - 1; // без последней `
const contentStart = start; // без первой `
const contentEnd = end; // без последней `
// Find code blocks to ignore
if (this.isInsideCodeBlock(contentStart, contentEnd, codeBlocks)) {

View file

@ -1,4 +1,4 @@
import { JiraIssue } from "../interfaces";
import {JiraIssue} from "../interfaces";
import {jiraToMarkdown} from "./markdownHtml";
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
@ -64,12 +64,14 @@ export async function updateJiraToLocal(
// Update content sections from Jira fields
let updatedContent = fileContent;
let updatesDict: Record<string, string> = {};
for (const [fieldName, fieldValue] of Object.entries(syncSections)) {
const markdownValue = jiraToMarkdown(fieldValue);
debugLog(`Updating sync section: ${fieldName}: ${markdownValue}`);
updatedContent = updateJiraSyncContent(updatedContent, fieldName, markdownValue);
updatesDict[fieldName] = jiraToMarkdown(fieldValue);
}
debugLog(`Updating sync sections: ${JSON.stringify(updatesDict)}`);
updatedContent = updateJiraSyncContent(updatedContent, updatesDict);
return updatedContent;
});
}

View file

@ -1,114 +1,128 @@
import { debugLog } from "./debugLogging";
const regexCache = new Map<string, RegExp>();
const UNIFIED_REGEX = /`jira-sync-(section|line|inline-start|block-start)-([\w-]+)`([\s\S]*?)(?:`jira-sync-?[^-]*-end`|(?=`jira-sync-|\z))/g; // TODO: delete deprecated endings in a year
function getRegex(type: "section" | "line" | "inline", name: string): RegExp {
const key = `${type}:${name}`;
if (regexCache.has(key)) return regexCache.get(key)!;
let regex: RegExp;
switch (type) {
case "section":
regex = new RegExp(
`\`jira-sync-section-${name}\`([\\s\\S]*?)\\n?([\\s\\S]*?)(?=\\n#+ |\\n[^\\n]*\`jira-sync-|$)`,
"g"
);
break;
case "line":
regex = new RegExp(
`\`jira-sync-line-${name}\` *(.*?)(?=\\n|$)`,
"g"
);
break;
case "inline":
regex = new RegExp(
`\`jira-sync-inline-start-${name}\`([\\s\\S]*?)\`jira-sync-inline-end\``,
"g"
);
break;
}
regexCache.set(key, regex);
return regex;
interface ParsedBlock {
type: "section" | "line" | "inline" | "block";
name: string;
content: string;
startIndex: number;
endIndex: number;
fullMatch: string;
}
function updateJiraSyncBlock(
fileContent: string,
type: "section" | "line" | "inline",
name: string,
content: string,
force: boolean
): string {
const regex = getRegex(type, name);
let newBlock: string;
switch (type) {
case "section":
newBlock = `\`jira-sync-section-${name}\`\n${content}`;
break;
case "line":
newBlock = `\`jira-sync-line-${name}\`${content.split("\n")[0]}`;
break;
case "inline":
newBlock = `\`jira-sync-inline-start-${name}\`${content}\`jira-sync-inline-end\``;
break;
}
const updated = fileContent.replace(regex, () => newBlock);
if (updated !== fileContent) return updated;
if (force) {
return fileContent + `\n${newBlock}`;
}
return fileContent;
}
export function updateJiraSyncContent(
fileContent: string,
sectionName: string,
markdownContent: string,
force: boolean = false
): string {
fileContent = updateJiraSyncBlock(fileContent, "section", sectionName, markdownContent, force);
fileContent = updateJiraSyncBlock(fileContent, "line", sectionName, markdownContent, force);
fileContent = updateJiraSyncBlock(fileContent, "inline", sectionName, markdownContent, force);
return fileContent;
}
function extractValues(
fileContent: string,
type: "section" | "line" | "inline"
): Record<string, string> {
const regex = getRegex(type, "([\\w-]+)");
const values: Record<string, string> = {};
export function parseFileContent(fileContent: string): ParsedBlock[] {
const blocks: ParsedBlock[] = [];
let match;
while ((match = regex.exec(fileContent)) !== null) {
switch (type) {
case "section":
values[match[1]] = (match[3] || "").trim();
break;
case "line":
values[match[1]] = (match[2] || "").trim();
break;
case "inline":
values[match[1]] = (match[2] || "").trim();
break;
// Reset regex state
UNIFIED_REGEX.lastIndex = 0;
while ((match = UNIFIED_REGEX.exec(fileContent)) !== null) {
const [fullMatch, typeRaw, name, rawContent] = match;
let type: ParsedBlock["type"];
let extractedContent: string;
let actualEndIndex: number;
// Determine actual type and extract content
if (typeRaw === "section") {
type = "section";
// Cut at first heading or another jira-sync-*
const headingMatch = rawContent.match(/\n#+? /);
const contentBeforeHeading = headingMatch
? rawContent.substring(0, (headingMatch.index||0)+1)
: rawContent;
actualEndIndex = match.index + fullMatch.length - (rawContent.length - contentBeforeHeading.length);
extractedContent = contentBeforeHeading.trim();
} else if (typeRaw === "line") {
type = "line";
// Content is on the same line only
const firstLine = rawContent.split("\n")[0];
actualEndIndex = match.index + fullMatch.length - (rawContent.length - firstLine.length);
extractedContent = firstLine.trim();
} else if (typeRaw === "inline-start") {
type = "inline";
// Content between inline-start and inline-end (markers included in fullMatch)
actualEndIndex = match.index + fullMatch.length;
extractedContent = rawContent.trim();
} else if (typeRaw === "block-start") {
type = "block";
// Content between block-start and block-end
const lines = rawContent.split("\n");
const contentLines = lines.slice(lines[0] === "" ? 1 : 0, -1);
const joinedContent = contentLines.join("\n");
actualEndIndex = match.index + fullMatch.length;
extractedContent = joinedContent.trim();
} else {
continue;
}
blocks.push({
type,
name,
content: extractedContent,
startIndex: match.index,
endIndex: actualEndIndex,
// indexesContent: fileContent.substring(match.index, actualEndIndex),
fullMatch
});
}
return values;
return blocks;
}
export function extractAllJiraSyncValuesFromContent(
fileContent: string
): Record<string, string> {
const sections = extractValues(fileContent, "section");
const lines = extractValues(fileContent, "line");
const inlines = extractValues(fileContent, "inline");
const blocks = parseFileContent(fileContent);
const result: Record<string, string> = {};
for (const block of blocks) {
result[block.name] = block.content;
}
const result = { ...sections, ...lines, ...inlines };
debugLog(`extracted content from file: ${JSON.stringify(result)}`);
return result;
}
export function updateJiraSyncContent(
fileContent: string,
updates: Record<string, string>
): string {
const blocks = parseFileContent(fileContent);
// Sort blocks in reverse order to replace from end to start
// This prevents index shifting issues
blocks.sort((a, b) => b.startIndex - a.startIndex);
let result = fileContent;
for (const block of blocks) {
if (!(block.name in updates)) {
continue;
}
const newContent = updates[block.name];
let newBlock: string;
switch (block.type) {
case "section":
newBlock = `\`jira-sync-section-${block.name}\`\n${newContent}\n`;
break;
case "line":
newBlock = `\`jira-sync-line-${block.name}\`${newContent.split("\n")[0]}`;
break;
case "inline":
newBlock = `\`jira-sync-inline-start-${block.name}\`${newContent}\`jira-sync-end\``;
break;
case "block":
newBlock = `\`jira-sync-block-start-${block.name}\`\n${newContent}\n\`jira-sync-end\``;
break;
}
// Replace from end to start to avoid index shifting
result = result.substring(0, block.startIndex) + newBlock + result.substring(block.endIndex);
}
return result;
}