Repository: shikijs/shiki (ts)

Issue:
`transformerMetaWordHighlight` fail to highlight overlapping text instances
### Validations

- [x] Follow our [Code of Conduct](https://github.com/shikijs/shiki/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [Contributing Guide](https://github.com/shikijs/shiki/blob/main/CONTRIBUTING.md).
- [x] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.

### Describe the bug

When using `transformerMetaWordHighlight` to highlight words that overlap, Shiki fails.

Example:

```
```js /xx/
xxx
``` .
```

Shiki will try to create two `spans` with class `highlighted-word`: `(xx)x` and `x(xx)` and will result in the following error:

```
Failed to parse Markdown file "...":
Decorations {"line":0,"character":0,"offset":0} and {"line":0,"character":1,"offset":1} intersect.
```

Perhaps a way to fix is to allow highlighting words by character position?

On a related note, why the use of `/.../` that resembles regex?

### Reproduction

https://stackblitz.com/edit/shiki-intersect?file=src%2Fpages%2Findex.astro,astro.config.mjs,src%2Fcomponents%2Ftest.md

### Contributes

- [x] I am willing to submit a PR to fix this issue
- [ ] I am willing to submit a PR with failing tests

Pull request:
fix(transformers): fix matching indices for word-highlight

<!-- DO NOT IGNORE THE TEMPLATE!

Thank you for contributing!

Before submitting the PR, please make sure you do the following:

- Read the [Contributing Guide](https://github.com/shikijs/shiki/blob/main/CONTRIBUTING.md).
- Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
- Ideally, include relevant tests that fail without this PR but pass with it.

-->

### Description

Partially fixes #908 and add tests.

### Additional context

Users still cannot freely choose which instance the highlight should be on (regex will choose whatever comes up first).

It would be nice to allow users to manually specify which character (by index) the highlight should be. For example, `js {1[6:12],3-4} to highlight characters 6 through 12 of the first line, and lines 3+4. But that seems like a lot more work. At least this PR will eliminate the error.


Patch:
diff --git a/packages/transformers/src/transformers/meta-highlight-word.ts b/packages/transformers/src/transformers/meta-highlight-word.ts
index ce10b857..22562644 100644
--- a/packages/transformers/src/transformers/meta-highlight-word.ts
+++ b/packages/transformers/src/transformers/meta-highlight-word.ts
@@ -55,11 +55,17 @@ export function transformerMetaWordHighlight(
   }
 }
 
-function findAllSubstringIndexes(str: string, substr: string): number[] {
-  const indexes = []
-  let i = -1
-  // eslint-disable-next-line no-cond-assign
-  while ((i = str.indexOf(substr, i + 1)) !== -1)
-    indexes.push(i)
+export function findAllSubstringIndexes(str: string, substr: string): number[] {
+  const indexes: number[] = []
+  let cursor = 0
+  while (true) {
+    const index = str.indexOf(substr, cursor)
+    if (index === -1 || index >= str.length)
+      break
+    if (index < cursor)
+      break
+    indexes.push(index)
+    cursor = index + substr.length
+  }
   return indexes
 }


Test patch:
diff --git a/packages/transformers/test/meta-word-highlight.test.ts b/packages/transformers/test/meta-word-highlight.test.ts
index 7c48fcf0..d7f9dfa5 100644
--- a/packages/transformers/test/meta-word-highlight.test.ts
+++ b/packages/transformers/test/meta-word-highlight.test.ts
@@ -1,9 +1,11 @@
 import { expect, it } from 'vitest'
-import { parseMetaHighlightWords } from '../src/transformers/meta-highlight-word'
+import { findAllSubstringIndexes, parseMetaHighlightWords } from '../src/transformers/meta-highlight-word'
 
 it('parseHighlightWords', () => {
   expect(parseMetaHighlightWords('')).toEqual([])
   expect(parseMetaHighlightWords('/hello/')).toEqual(['hello'])
   expect(parseMetaHighlightWords('/ /f /hello/')).toEqual([' ', 'hello'])
   expect(parseMetaHighlightWords('/foo bar/ /foo.bar\\/foo/')).toEqual(['foo bar', 'foo.bar/foo'])
+  expect(findAllSubstringIndexes('xxx', 'xx')).toEqual([0])
+  expect(findAllSubstringIndexes('xxxx', 'xx')).toEqual([0, 2])
 })