@iyulab/chat-components 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.1] - 2026-09-30
4
+
5
+ ### Fixed
6
+
7
+ - **A citation whose index falls inside a link URL, an image, or inline code no longer breaks it.**
8
+ The citation markup was restored into the `href` attribute (the link pointed at a truncated URL
9
+ followed by markup) or into the `<code>` text. It is now placed right after the link, image, or code.
10
+ - **A citation that points past the text received so far is held until that text arrives.** During
11
+ streaming it used to be drawn at the end of the partial text and then jump to its place.
12
+ - A citation with a negative or non-integer index is ignored. A negative index used to be counted
13
+ from the end of the text and inserted mid-word.
14
+
3
15
  ## [0.14.0] - 2026-09-30
4
16
 
5
17
  ### Security
@@ -69,6 +69,8 @@ export declare class UMarkedBlock extends UElement {
69
69
  * 요청 자체가 반출 경로이므로 대체 텍스트만 그린다.
70
70
  */
71
71
  private renderImage;
72
+ /** 인라인 코드 안에 떨어진 인용은 코드 뒤로 옮긴다 — 코드는 글자 그대로여야 한다. */
73
+ private renderCodespan;
72
74
  /**
73
75
  * 인용/출처 참조객체가 있을때 ref 태그 HTML을 this.placeholder에 보관하고
74
76
  * 마크다운 문자열의 해당 위치에 플레이스홀더 센티널을 삽입합니다.
@@ -11,7 +11,7 @@ import { URefCard } from "../references/URefCard.js";
11
11
  import "../references/URefCardGroup.js";
12
12
  import { UTableBlock } from "./UTableBlock.js";
13
13
  import { resolveImageSource } from "../../utilities/imageSources.js";
14
- import { HtmlPlaceholder, stripRefPlaceholders } from "../../utilities/HtmlPlaceholder.js";
14
+ import { HtmlPlaceholder, extractRefPlaceholders, stripRefPlaceholders } from "../../utilities/HtmlPlaceholder.js";
15
15
  import { styles } from "./UMarkedBlock.styles.js";
16
16
  import { nothing } from "lit";
17
17
  import { customElement, property, state } from "lit/decorators.js";
@@ -32,7 +32,8 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
32
32
  table: (token) => this.renderTable(token),
33
33
  html: (token) => this.renderHtml(token),
34
34
  link: (token) => this.renderLink(token),
35
- image: (token) => this.renderImage(token)
35
+ image: (token) => this.renderImage(token),
36
+ codespan: (token) => this.renderCodespan(token)
36
37
  }
37
38
  }).use(markedKatex({ output: "mathml" }));
38
39
  this.placeholder = new HtmlPlaceholder();
@@ -130,10 +131,12 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
130
131
  */
131
132
  renderLink(token) {
132
133
  const text = token.tokens ? Parser.parseInline(token.tokens, this.parser.defaults) : token.text;
133
- let out = `<a href="${escapeHtmlHref(token.href)}"`;
134
- if (token.title) out += ` title="${escapeHtmlAttr(token.title)}"`;
134
+ const [href, inHref] = extractRefPlaceholders(token.href);
135
+ const [title, inTitle] = extractRefPlaceholders(token.title ?? "");
136
+ let out = `<a href="${escapeHtmlHref(href)}"`;
137
+ if (title) out += ` title="${escapeHtmlAttr(title)}"`;
135
138
  out += `>${text}</a>`;
136
- return out;
139
+ return out + inHref + inTitle;
137
140
  }
138
141
  /**
139
142
  * 마크다운 이미지의 src도 `renderLink`와 동일하게 protocol을 검사하고, 이미지 출처 정책
@@ -141,22 +144,31 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
141
144
  * 요청 자체가 반출 경로이므로 대체 텍스트만 그린다.
142
145
  */
143
146
  renderImage(token) {
144
- const src = resolveImageSource(token.href);
147
+ const [href, inHref] = extractRefPlaceholders(token.href);
148
+ const [alt, inAlt] = extractRefPlaceholders(token.text);
149
+ const [title, inTitle] = extractRefPlaceholders(token.title ?? "");
150
+ const refs = inHref + inAlt + inTitle;
151
+ const src = resolveImageSource(href);
145
152
  if (src === null) {
146
- const label = token.text ? messages.text("imageBlockedNamed", { name: token.text }) : messages.text("imageBlocked");
147
- return `<span class="image-blocked" part="image-blocked">${escapeHtmlText(label)}</span>`;
153
+ const label = alt ? messages.text("imageBlockedNamed", { name: alt }) : messages.text("imageBlocked");
154
+ return `<span class="image-blocked" part="image-blocked">${escapeHtmlText(label)}</span>` + refs;
148
155
  }
149
- let out = `<img src="${escapeHtmlAttr(src)}" alt="${escapeHtmlAttr(token.text)}"`;
150
- if (token.title) out += ` title="${escapeHtmlAttr(token.title)}"`;
156
+ let out = `<img src="${escapeHtmlAttr(src)}" alt="${escapeHtmlAttr(alt)}"`;
157
+ if (title) out += ` title="${escapeHtmlAttr(title)}"`;
151
158
  out += `>`;
152
- return out;
159
+ return out + refs;
160
+ }
161
+ /** 인라인 코드 안에 떨어진 인용은 코드 뒤로 옮긴다 — 코드는 글자 그대로여야 한다. */
162
+ renderCodespan(token) {
163
+ const [text, refs] = extractRefPlaceholders(token.text);
164
+ return `<code>${escapeHtmlText(text)}</code>${refs}`;
153
165
  }
154
166
  /**
155
167
  * 인용/출처 참조객체가 있을때 ref 태그 HTML을 this.placeholder에 보관하고
156
168
  * 마크다운 문자열의 해당 위치에 플레이스홀더 센티널을 삽입합니다.
157
169
  */
158
170
  insertRefs(value, refs) {
159
- const sorted = [...refs].sort((a, b) => b.endIndex - a.endIndex);
171
+ const sorted = refs.filter((r) => Number.isInteger(r.endIndex) && r.endIndex >= 0 && r.endIndex <= value.length).sort((a, b) => b.endIndex - a.endIndex);
160
172
  for (const ref of sorted) {
161
173
  const sources = ref.sources ?? [];
162
174
  const label = escapeHtmlText(ref.label ?? `[${refs.indexOf(ref) + 1}]`);
@@ -11,6 +11,14 @@
11
11
  * 종전처럼 한쪽만 바뀌어 어긋난다.
12
12
  */
13
13
  export declare function stripRefPlaceholders(value: string): string;
14
+ /**
15
+ * 문자열에서 ref 플레이스홀더 센티널을 떼어 낸다 — `[센티널 없는 문자열, 떼어 낸 센티널들]`.
16
+ *
17
+ * 링크 주소·이미지 속성·인라인 코드처럼 **태그가 들어갈 수 없는 자리**에 인용이 떨어졌을 때 쓴다.
18
+ * 그 자리에 남기면 `restore()` 가 속성 값이나 `<code>` 안에 HTML 을 넣는다(링크가 깨지고 코드에
19
+ * 인용 태그가 섞인다). 지우면 인용을 잃으므로, 호출부가 떼어 낸 것을 그 구문 **바로 뒤**에 둔다.
20
+ */
21
+ export declare function extractRefPlaceholders(value: string): [string, string];
14
22
  export declare class HtmlPlaceholder {
15
23
  private map;
16
24
  private idx;
@@ -50,6 +50,18 @@ var sentinelPattern = () => /\u{E000}\d+\u{E001}/gu;
50
50
  function stripRefPlaceholders(value) {
51
51
  return value.replace(sentinelPattern(), "");
52
52
  }
53
+ /**
54
+ * 문자열에서 ref 플레이스홀더 센티널을 떼어 낸다 — `[센티널 없는 문자열, 떼어 낸 센티널들]`.
55
+ *
56
+ * 링크 주소·이미지 속성·인라인 코드처럼 **태그가 들어갈 수 없는 자리**에 인용이 떨어졌을 때 쓴다.
57
+ * 그 자리에 남기면 `restore()` 가 속성 값이나 `<code>` 안에 HTML 을 넣는다(링크가 깨지고 코드에
58
+ * 인용 태그가 섞인다). 지우면 인용을 잃으므로, 호출부가 떼어 낸 것을 그 구문 **바로 뒤**에 둔다.
59
+ */
60
+ function extractRefPlaceholders(value) {
61
+ const keys = value.match(sentinelPattern());
62
+ if (!keys) return [value, ""];
63
+ return [value.replace(sentinelPattern(), ""), keys.join("")];
64
+ }
53
65
  var HtmlPlaceholder = class {
54
66
  constructor() {
55
67
  this.map = {};
@@ -78,4 +90,4 @@ var HtmlPlaceholder = class {
78
90
  }
79
91
  };
80
92
  //#endregion
81
- export { HtmlPlaceholder, stripRefPlaceholders };
93
+ export { HtmlPlaceholder, extractRefPlaceholders, stripRefPlaceholders };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/chat-components",
3
3
  "description": "llm chat components for building chat interfaces",
4
- "version": "0.14.0",
4
+ "version": "0.14.1",
5
5
  "author": "iyulab",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -79,6 +79,11 @@ Markdown images (table cells included) follow the page-wide image origin policy
79
79
 
80
80
  ## ReferenceCitation Type
81
81
 
82
+ A citation is drawn at `endIndex` in the markdown source. While a response streams, a citation whose
83
+ `endIndex` is past the text received so far is held until that text arrives; a negative or non-integer
84
+ index is ignored. If `endIndex` falls inside a link URL, an image, or inline code, the citation is drawn
85
+ right after that element; inside a fenced code block it is dropped.
86
+
82
87
  ```ts
83
88
  interface ReferenceCitation {
84
89
  startIndex: number; // Start character index in the markdown text