Repository: shikijs/shiki
Pull request #905 (2025-01-23 17:37:34)

Update astro.md
update astro docs syntax highlighting link


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-matsu* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 3eafabc1a671bb8c4809e2e7ea01b6d3b9608624 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-matsu/deploys/67927e603afbf500089a2582 |
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-next* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 3eafabc1a671bb8c4809e2e7ea01b6d3b9608624 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-next/deploys/67927e61b9c05e0008c7f427 |

Patch:
diff --git a/docs/packages/astro.md b/docs/packages/astro.md
index 64a3779d..ad69f03e 100644
--- a/docs/packages/astro.md
+++ b/docs/packages/astro.md
@@ -2,4 +2,4 @@
 
 You don't need explicit integration in [Astro](https://astro.build/) as Astro already uses Shiki under the hood.
 
-Learn more about the configurations in the [Astro documentation](https://docs.astro.build/en/guides/markdown-content/#syntax-highlighting).
+Learn more about the configurations in the [Astro documentation](https://docs.astro.build/en/guides/syntax-highlighting/).
diff --git a/packages/core/src/transformer-decorations.ts b/packages/core/src/transformer-decorations.ts
index bf639c7d..4cad7a8f 100644
--- a/packages/core/src/transformer-decorations.ts
+++ b/packages/core/src/transformer-decorations.ts
@@ -166,7 +166,7 @@ export function transformerDecorations(): ShikiTransformer {
       const lineApplies: (() => void)[] = []
 
       // Apply decorations in reverse order so the nested ones get applied first.
-      const sorted = ctx.decorations.sort((a, b) => b.start.offset - a.start.offset)
+      const sorted = ctx.decorations.sort((a, b) => b.start.offset - a.start.offset || a.end.offset - b.end.offset)
       for (const decoration of sorted) {
         const { start, end } = decoration
         if (start.line === end.line) {
@@ -193,12 +193,12 @@ function verifyIntersections(items: ResolvedDecorationItem[]): void {
 
     for (let j = i + 1; j < items.length; j++) {
       const bar = items[j]
-      const isFooHasBarStart = foo.start.offset < bar.start.offset && bar.start.offset < foo.end.offset
-      const isFooHasBarEnd = foo.start.offset < bar.end.offset && bar.end.offset < foo.end.offset
-      const isBarHasFooStart = bar.start.offset < foo.start.offset && foo.start.offset < bar.end.offset
-      const isBarHasFooEnd = bar.start.offset < foo.end.offset && foo.end.offset < bar.end.offset
+      const isFooHasBarStart = foo.start.offset <= bar.start.offset && bar.start.offset < foo.end.offset
+      const isFooHasBarEnd = foo.start.offset < bar.end.offset && bar.end.offset <= foo.end.offset
+      const isBarHasFooStart = bar.start.offset <= foo.start.offset && foo.start.offset < bar.end.offset
+      const isBarHasFooEnd = bar.start.offset < foo.end.offset && foo.end.offset <= bar.end.offset
       if (isFooHasBarStart || isFooHasBarEnd || isBarHasFooStart || isBarHasFooEnd) {
-        if (isFooHasBarEnd && isFooHasBarEnd)
+        if (isFooHasBarStart && isFooHasBarEnd)
           continue // nested
         if (isBarHasFooStart && isBarHasFooEnd)
           continue // nested
diff --git a/packages/rehype/src/core.ts b/packages/rehype/src/core.ts
index d8d49ee7..c9e2304f 100644
--- a/packages/rehype/src/core.ts
+++ b/packages/rehype/src/core.ts
@@ -98,30 +98,29 @@ function rehypeShikiFromHighlighter(
       if (node.tagName === 'pre') {
         handler = PreHandler
       }
-
-      if (node.tagName === 'code' && inline) {
+      else if (node.tagName === 'code' && inline) {
         handler = InlineCodeHandlers[inline]
       }
-
-      if (!handler)
+      else {
         return
+      }
 
-      const res = handler(tree, node)
-      if (!res)
+      const parsed = handler(tree, node)
+      if (!parsed)
         return
 
       let lang: string | undefined
       let lazyLoad = false
 
-      if (!res.lang) {
+      if (!parsed.lang) {
         lang = defaultLanguage
       }
-      else if (highlighter.getLoadedLanguages().includes(res.lang) || isSpecialLang(res.lang)) {
-        lang = res.lang
+      else if (highlighter.getLoadedLanguages().includes(parsed.lang) || isSpecialLang(parsed.lang)) {
+        lang = parsed.lang
       }
       else if (lazy) {
         lazyLoad = true
-        lang = res.lang
+        lang = parsed.lang
       }
       else if (fallbackLanguage) {
         lang = fallbackLanguage
@@ -130,14 +129,14 @@ function rehypeShikiFromHighlighter(
       if (!lang)
         return
 
-      const processNode = (): void => {
-        const meta = res.meta ? parseMetaString?.(res.meta, node, tree) : undefined
+      const meta = parsed.meta ? parseMetaString?.(parsed.meta, node, tree) : undefined
 
-        const fragment = highlight(lang, res.code, res.meta, meta ?? {})
+      const processNode = (targetLang: string): void => {
+        const fragment = highlight(targetLang, parsed.code, parsed.meta, meta ?? {})
         if (!fragment)
           return
 
-        if (res.type === 'inline') {
+        if (parsed.type === 'inline') {
           const head = fragment.children[0]
           if (head.type === 'element' && head.tagName === 'pre') {
             head.tagName = 'span'
@@ -148,10 +147,20 @@ function rehypeShikiFromHighlighter(
       }
 
       if (lazyLoad) {
-        queue.push(highlighter.loadLanguage(lang).then(() => processNode()))
+        try {
+          // passed language is checked in sync, promise `.catch()` wouldn't work
+          queue.push(highlighter.loadLanguage(lang).then(() => processNode(lang)))
+        }
+        catch (error) {
+          if (fallbackLanguage)
+            return processNode(fallbackLanguage)
+          else if (onError)
+            onError(error)
+          else throw error
+        }
       }
       else {
-        processNode()
+        processNode(lang)
       }
 
       // don't visit processed nodes
diff --git a/packages/rehype/src/types.ts b/packages/rehype/src/types.ts
index cd34223c..92c6e4f8 100644
--- a/packages/rehype/src/types.ts
+++ b/packages/rehype/src/types.ts
@@ -26,9 +26,7 @@ export interface RehypeShikiExtraOptions {
   defaultLanguage?: string
 
   /**
-   * The fallback language to use when specified language is not loaded
-   *
-   * Ignored if `lazy` is enabled
+   * The fallback language to use when specified language is not loaded, or not included in the bundle
    */
   fallbackLanguage?: string
 


Test patch:
diff --git a/packages/core/test/decorations.test.ts b/packages/core/test/decorations.test.ts
index 7664caa5..d5e65a35 100644
--- a/packages/core/test/decorations.test.ts
+++ b/packages/core/test/decorations.test.ts
@@ -78,6 +78,20 @@ describe('decorations', () => {
           end: { line: 8, character: 25 },
           properties: { class: 'highlighted' },
         },
+        // "hast"
+        // Testing nested decorations with shared start.
+        {
+          start: { line: 8, character: 15 },
+          end: { line: 8, character: 19 },
+          properties: { class: 'highlighted' },
+        },
+        // "Html"
+        // Testing nested decorations with shared end.
+        {
+          start: { line: 8, character: 21 },
+          end: { line: 8, character: 25 },
+          properties: { class: 'highlighted' },
+        },
         // "// final"
         // Testing offset === code.length edge case
         {
@@ -117,12 +131,12 @@ describe('decorations errors', () => {
         lang: 'ts',
         decorations: [
           { start: 0, end: 10 },
-          { start: 5, end: 15 },
+          { start: 1, end: 11 },
         ],
       })
     })
       .rejects
-      .toThrowErrorMatchingInlineSnapshot(`[ShikiError: Decorations {"line":0,"character":0,"offset":0} and {"line":1,"character":1,"offset":5} intersect.]`)
+      .toThrowErrorMatchingInlineSnapshot(`[ShikiError: Decorations {"line":0,"character":0,"offset":0} and {"line":0,"character":1,"offset":1} intersect.]`)
   })
 
   it('throws when lines overflow', async () => {
diff --git a/packages/core/test/out/decorations/basic.html b/packages/core/test/out/decorations/basic.html
index 324202fc..fad465be 100644
--- a/packages/core/test/out/decorations/basic.html
+++ b/packages/core/test/out/decorations/basic.html
@@ -15,7 +15,7 @@
 <span class="line"><span style="color:#B07D48"> </span><span style="filter: invert(1)" class="highlighted"> co</span><span style="color:#B07D48">de</span><span style="color:#999999">: </span><span style="color:#2E8F82">string</span><span style="color:#999999">,</span></span>
 <span class="line"><span style="color:#B07D48">  options</span><span style="color:#999999">: </span><span style="color:#2E8F82">CodeToHastOptions</span><span style="color:#999999">,</span></span>
 <span class="line"><span style="color:#999999">):</span><span style="color:#2E8F82"> string</span><span style="color:#999999"> </span><span style="color:#999999" class="highlighted-body">{</span></span>
-<span class="line highlighted-body"><span style="color:#AB5959">  let </span><span style="color:#B07D48">result</span><span style="color:#999999"> =</span><span style="color:#59873A"> </span><span style="color:#59873A" class="highlighted">hastToHtml</span><span style="color:#999999">(</span><span style="color:#59873A">codeToHast</span><span style="color:#999999">(</span><span style="color:#B07D48">internal</span><span style="color:#999999">,</span><span style="color:#B07D48"> code</span><span style="color:#999999">,</span><span style="color:#B07D48"> options</span><span style="color:#999999">,</span><span style="color:#B07D48"> context</span><span style="color:#999999">))</span></span>
+<span class="line highlighted-body"><span style="color:#AB5959">  let </span><span style="color:#B07D48">result</span><span style="color:#999999"> =</span><span style="color:#59873A"> </span><span class="highlighted"><span style="color:#59873A" class="highlighted">hast</span><span style="color:#59873A">To</span><span style="color:#59873A" class="highlighted">Html</span></span><span style="color:#999999">(</span><span style="color:#59873A">codeToHast</span><span style="color:#999999">(</span><span style="color:#B07D48">internal</span><span style="color:#999999">,</span><span style="color:#B07D48"> code</span><span style="color:#999999">,</span><span style="color:#B07D48"> options</span><span style="color:#999999">,</span><span style="color:#B07D48"> context</span><span style="color:#999999">))</span></span>
 <span class="line highlighted-body"><span style="color:#1E754F">  return</span><span style="color:#B07D48"> result</span></span>
 <span class="line highlighted-body"><span style="color:#999999">}</span></span>
 <span class="line highlighted"><span style="color:#A0ADA0">// final</span></span></code></pre>
\ No newline at end of file
diff --git a/packages/rehype/test/core.test.ts b/packages/rehype/test/core.test.ts
index 0878764c..f19485a5 100644
--- a/packages/rehype/test/core.test.ts
+++ b/packages/rehype/test/core.test.ts
@@ -99,3 +99,27 @@ it('run with rehype-raw', async () => {
 
   await expect(file.toString()).toMatchFileSnapshot('./fixtures/a.core.out.html')
 })
+
+it('run with lazy + fallback language', async () => {
+  const highlighter = await createHighlighter({
+    themes: [
+      'vitesse-light',
+    ],
+    langs: [],
+  })
+
+  const file = await unified()
+    .use(remarkParse)
+    .use(remarkRehype)
+    .use(rehypeShikiFromHighlighter, highlighter, {
+      lazy: true,
+      theme: 'vitesse-light',
+      defaultLanguage: 'text',
+      fallbackLanguage: 'text',
+      langs: [],
+    })
+    .use(rehypeStringify)
+    .process(await fs.readFile(new URL('./fixtures/d.md', import.meta.url)))
+
+  await expect(file.toString()).toMatchFileSnapshot('./fixtures/d.out.html')
+})
diff --git a/packages/rehype/test/fixtures/d.md b/packages/rehype/test/fixtures/d.md
new file mode 100644
index 00000000..ff507daa
--- /dev/null
+++ b/packages/rehype/test/fixtures/d.md
@@ -0,0 +1,3 @@
+```invalid
+Should use fallback language (plaintext)
+```
\ No newline at end of file
diff --git a/packages/rehype/test/fixtures/d.out.html b/packages/rehype/test/fixtures/d.out.html
new file mode 100644
index 00000000..6aa0cf2c
--- /dev/null
+++ b/packages/rehype/test/fixtures/d.out.html
@@ -0,0 +1,1 @@
+<pre class="shiki vitesse-light" style="background-color:#ffffff;color:#393a34" tabindex="0"><code><span class="line"><span>Should use fallback language (plaintext)</span></span></code></pre>
\ No newline at end of file