siyuan/kernel/bazaar
2026-03-06 17:44:20 +08:00
..
icon.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00
package.go 🎨 Normalize bazaar naming and reorganize kernel/bazaar structure (#17133) 2026-03-04 20:53:06 +08:00
plugin.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00
readme.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00
stage.go 🎨 Normalize bazaar naming and reorganize kernel/bazaar structure (#17133) 2026-03-04 20:53:06 +08:00
template.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00
theme.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00
widget.go 🎨 Add the code-block class name when rendering code blocks in the market README (#17145) 2026-03-06 17:44:20 +08:00

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package bazaar

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/88250/gulu"
	"github.com/88250/lute"
	"github.com/88250/lute/ast"
	"github.com/88250/lute/parse"
	"github.com/siyuan-note/logging"
	"github.com/siyuan-note/siyuan/kernel/util"
	textUnicode "golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

// getReadmeFileCandidates 根据包的 README 配置返回去重的按优先级排序的 README 候选文件名列表当前语言首选、default、README.md。
func getReadmeFileCandidates(readme LocaleStrings) []string {
	preferred := getPreferredReadme(readme)
	defaultName := "README.md"
	if v := strings.TrimSpace(readme["default"]); v != "" {
		defaultName = v
	}
	return gulu.Str.RemoveDuplicatedElem([]string{preferred, defaultName, "README.md"})
}

// GetBazaarPackageREADME 获取集市包的在线 README。
func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, packageType string) (ret string) {
	repoURLHash := repoURL + "@" + repoHash

	stageIndexLock.RLock()
	stageIndex := cachedStageIndex[packageType]
	stageIndexLock.RUnlock()
	if stageIndex == nil {
		var err error
		stageIndex, err = getStageIndex(ctx, packageType)
		if err != nil {
			return
		}
	}

	url := strings.TrimPrefix(repoURLHash, "https://github.com/")
	var repo *StageRepo
	for _, r := range stageIndex.Repos {
		if r.URL == url {
			repo = r
			break
		}
	}
	if repo == nil || repo.Package == nil {
		return
	}

	candidates := getReadmeFileCandidates(repo.Package.Readme)
	var data []byte
	var loadErr error
	var errMsgs []string
	for _, name := range candidates {
		data, loadErr = downloadPackage(repoURLHash+"/"+name, false, "")
		if loadErr == nil {
			break
		}
		errMsgs = append(errMsgs, fmt.Sprintf("Load bazaar package's README(%s) failed: %s", name, loadErr.Error()))
	}
	if loadErr != nil {
		ret = strings.Join(errMsgs, "<br>")
		return
	}

	if len(data) > 2 {
		var decoded []byte
		var err error
		if data[0] == 0xFF && data[1] == 0xFE {
			decoded, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
		} else if data[0] == 0xFE && data[1] == 0xFF {
			decoded, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
		}
		if decoded != nil && err == nil {
			data = decoded
		}
	}

	linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
	ret = renderPackageREADME(linkBase, data)
	return
}

// getInstalledPackageREADME 获取集市包的本地 README。
func getInstalledPackageREADME(installPath, linkBase string, readme LocaleStrings) (ret string) {
	candidates := getReadmeFileCandidates(readme)
	var errMsgs []string
	for _, name := range candidates {
		readmeData, readErr := os.ReadFile(filepath.Join(installPath, name))
		if readErr == nil {
			ret = renderPackageREADME(linkBase, readmeData)
			return
		}
		logging.LogWarnf("read installed %s failed: %s", name, readErr)
		errMsgs = append(errMsgs, fmt.Sprintf("File [%s] not found", name))
	}
	ret = strings.Join(errMsgs, "<br>")
	return
}

// renderPackageREADME 渲染 README Markdown 为 HTML。
func renderPackageREADME(linkBase string, mdData []byte) (ret string) {
	mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf")) // 移除文件开头的 BOM
	luteEngine := lute.New()
	luteEngine.SetSoftBreak2HardBreak(false)
	luteEngine.SetCodeSyntaxHighlight(false)
	luteEngine.SetLinkBase(linkBase)

	tree := parse.Parse("", mdData, luteEngine.ParseOptions)
	normalizeNodesIAL(tree)
	ret = luteEngine.Tree2HTML(tree, luteEngine.RenderOptions, luteEngine.ParseOptions)
	ret = util.LinkTarget(ret, linkBase)
	return
}

func normalizeNodesIAL(tree *parse.Tree) {
	if tree == nil || tree.Root == nil {
		return
	}

	ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
		if !entering {
			return ast.WalkContinue
		}
		if n.Type == ast.NodeCodeBlock {
			// 代码块添加 code-block 类名以修正样式。
			n.KramdownIAL = addClassToKramdownIAL(n.KramdownIAL, "code-block")
		}
		return ast.WalkContinue
	})
}

func addClassToKramdownIAL(ial [][]string, class string) [][]string {
	for i, attr := range ial {
		if len(attr) < 2 || attr[0] != "class" {
			continue
		}
		for _, item := range strings.Fields(attr[1]) {
			if item == class {
				return ial
			}
		}
		attr[1] = strings.TrimSpace(attr[1] + " " + class)
		ial[i] = attr
		return ial
	}
	return append(ial, []string{"class", class})
}