'read',
'channelType' => 'translation',
'contentType' => 'markdown',
'format' => 'react',
'debug' => [],
'studioId' => null,
'lang' => 'zh-Hans',
'footnote' => false,
'paragraph' => false,
'origin' => true,
'translation' => true,
];
/**
* Create a new command instance.
* string $mode 'read' | 'edit'
* string $format 'react' | 'text' | 'tex' | 'unity'
*
* @return void
*/
public function __construct(array $param, $channelInfo, string $mode, string $format = 'react', ?string $studioId = null, $debug = [], $lang = 'zh-Hans')
{
$this->param = $param;
foreach ($channelInfo as $channel) {
if ($channel && isset($channel->uid)) {
$this->channel_id[] = $channel->uid;
} else {
Log::error('template render init error invalid channel');
}
}
$this->channelInfo = $channelInfo;
$this->mode = $mode;
$this->format = $format;
$this->studioId = $studioId;
$this->debug = $debug;
$this->glossaryKey = 'glossary';
if (count($this->channel_id) > 0) {
$channelId = $this->channel_id[0];
if (Str::isUuid($channelId)) {
$lang = Channel::where('uid', $channelId)->value('lang');
}
}
if (! empty($lang)) {
$this->lang = $lang;
$this->langFamily = explode('-', $lang)[0];
}
}
public function options($options = [])
{
foreach ($options as $key => $value) {
$this->options[$key] = $value;
}
}
public function glossaryKey()
{
return $this->glossaryKey;
}
/**
* TODO 设置默认语言。在渲染某些内容的时候需要语言信息
*/
public function setLang($lang)
{
$this->lang = $lang;
$this->langFamily = explode('-', $lang)[0];
}
private function info($message, $debug)
{
if (in_array($debug, $this->debug)) {
Log::info($message);
}
}
private function error($message, $debug)
{
if (in_array($debug, $this->debug)) {
Log::error($message);
}
}
public function render($tpl_name)
{
switch ($tpl_name) {
case 'term':
// 术语
$result = $this->render_term();
break;
case 'note':
$result = $this->render_note();
break;
case 'sent':
$result = $this->render_sent();
break;
case 'quote':
$result = $this->render_quote();
break;
case 'ql':
$result = $this->render_quote_link();
break;
case 'exercise':
$result = $this->render_exercise();
break;
case 'article':
$result = $this->render_article();
break;
case 'nissaya':
$result = $this->render_nissaya();
break;
case 'mermaid':
$result = $this->render_mermaid();
break;
case 'qa':
$result = $this->render_qa();
break;
case 'v':
$result = $this->render_video();
break;
case 'g':
$result = $this->render_grammar_lookup();
break;
case 'ref':
$result = $this->render_ref();
break;
case 'dict-pref':
$result = $this->render_dict_pref();
break;
case 'ai':
$result = $this->render_ai();
break;
case 'para':
$result = $this->render_para();
break;
case 'category':
$result = $this->render_category();
break;
case 'quality':
// 质量标签模版:{{quality|pending}}
$result = $this->render_quality();
break;
default:
if (mb_substr($tpl_name, 0, 4, 'UTF-8') === 'Tpl:') {
$result = $this->render_tpl($tpl_name);
} else {
$result = [
'props' => base64_encode(\json_encode([])),
'html' => '',
'tag' => 'span',
'tpl' => 'unknown',
];
}
break;
}
return $result;
}
public function render_tpl($name)
{
$article = app(ArticleService::class)->getRawByTitle($name);
$content = $article->content;
if (count($this->param) > 0) {
$m = new \Mustache_Engine([
'entity_flags' => ENT_QUOTES,
'escape' => function ($value) {
return $value;
},
]);
$content = $m->render($content, $this->param);
}
$output = [];
switch ($this->format) {
case 'react':
$output = [
'props' => base64_encode(\json_encode(['content' => $content])),
'html' => $content,
'tag' => 'span',
'tpl' => 'tpl',
];
break;
default:
$output = $content;
break;
}
return $output;
}
public function render_para()
{
$props = [];
$props['id'] = $this->get_param($this->param, 'id', 1);
$props['title'] = $this->get_param($this->param, 'title', 2);
$props['style'] = $this->get_param($this->param, 'style', 3);
if (! empty($this->channel_id)) {
$props['channel'] = implode('_', $this->channel_id);
}
$output = [];
switch ($this->format) {
case 'react':
$output = [
'props' => base64_encode(\json_encode($props)),
'html' => $props['title'],
'tag' => 'span',
'tpl' => 'para',
];
break;
default:
$output = $props['title'];
break;
}
return $output;
}
public function render_category()
{
$props = [];
$props['name'] = $this->get_param($this->param, 'name', 1);
$output = [];
switch ($this->format) {
case 'react':
$output = [
'props' => base64_encode(\json_encode($props)),
'html' => $props['name'],
'tag' => 'span',
'tpl' => 'para',
];
break;
default:
$output = $props['name'];
break;
}
return $output;
}
/**
* 渲染质量标签模版:{{quality|pending}}
*
* 合法的取值只有四个:featured | standard | draft | pending
* 非法取值统一回退为 pending,避免渲染出未知样式。
*/
public function render_quality()
{
// 默认值为 pending,同时支持命名参数 {{quality|value=featured}}
$value = $this->get_param($this->param, 'value', 1, 'pending');
// 校验取值是否合法,不合法则回退到默认值
$allowed = ['featured', 'standard', 'draft', 'pending'];
if (! in_array($value, $allowed, true)) {
$value = 'pending';
}
$props = ['value' => $value];
$output = [];
switch ($this->format) {
case 'react':
// 前端通过 props 解析,使用 antd Tag 按取值渲染不同颜色
$output = [
'props' => base64_encode(\json_encode($props)),
'html' => $value,
'tag' => 'span',
'tpl' => 'quality',
];
break;
case 'html':
// 静态 html 输出,class 包含固定前缀 tag-quality 和具体取值
$output = "{$value}";
break;
default:
// text / tex / simple 等纯文本格式直接输出取值
$output = $value;
break;
}
return $output;
}
public function getTermProps($word, $tag = null, $channel = null)
{
if ($channel && ! empty($channel)) {
$channelId = $channel;
} else {
if (count($this->channel_id) > 0) {
$channelId = $this->channel_id[0];
} else {
$channelId = null;
}
}
if (count($this->channelInfo) === 0) {
if (! empty($channel)) {
$channelInfo = Channel::where('uid', $channel)->first();
if (! $channelInfo) {
unset($channelInfo);
}
}
if (! isset($channelInfo)) {
Log::warning('channel is null');
$output = [
'word' => $word,
'innerHtml' => '',
];
return $output;
}
} else {
$channelInfo = $this->channelInfo[0];
}
if (Str::isUuid($channelId)) {
$lang = Channel::where('uid', $channelId)->value('lang');
if (! empty($lang)) {
$langFamily = explode('-', $lang)[0];
} else {
$langFamily = 'zh';
}
$this->info("term:{$word} 先查属于这个channel 的", 'term');
$this->info('channel id'.$channelId, 'term');
$table = DhammaTerm::where('word', $word)
->where('channal', $channelId);
if ($tag && ! empty($tag)) {
$table = $table->where('tag', $tag);
}
$tplParam = $table->orderBy('updated_at', 'desc')
->first();
$studioId = $channelInfo->owner_uid;
} else {
$tplParam = false;
$lang = '';
$langFamily = '';
$studioId = $this->studioId;
}
if (! $tplParam) {
if (Str::isUuid($studioId)) {
/**
* 没有,再查这个studio的
* 按照语言过滤
* 完全匹配的优先
* 语族匹配也行
*/
$this->info('没有-再查这个studio的', 'term');
$table = DhammaTerm::where('word', $word);
if (! empty($tag)) {
$table = $table->where('tag', $tag);
}
$termsInStudio = $table->where('owner', $channelInfo->owner_uid)
->orderBy('updated_at', 'desc')
->get();
if (count($termsInStudio) > 0) {
$list = [];
foreach ($termsInStudio as $key => $term) {
if (empty($term->channal)) {
if ($term->language === $lang) {
$list[$term->guid] = 2;
} elseif (strpos($term->language, $langFamily) !== false) {
$list[$term->guid] = 1;
}
}
}
if (count($list) > 0) {
arsort($list);
foreach ($list as $key => $one) {
foreach ($termsInStudio as $term) {
if ($term->guid === $key) {
$tplParam = $term;
break;
}
}
break;
}
}
}
}
}
if (! $tplParam) {
$this->info('没有,再查社区', 'term');
$community_channel = ChannelApi::getSysChannel('_community_term_zh-hans_');
$table = DhammaTerm::where('word', $word);
if (! empty($tag)) {
$table = $table->where('tag', $tag);
}
$tplParam = $table->where('channal', $community_channel)
->first();
if ($tplParam) {
$isCommunity = true;
} else {
$this->info('查社区没有', 'term');
}
}
$output = [
'word' => $word,
'parentChannelId' => $channelId,
'parentStudioId' => $channelInfo ? $channelInfo->owner_uid : null,
];
$innerString = $output['word'];
if ($tplParam) {
$output['id'] = $tplParam->guid;
$output['meaning'] = $tplParam->meaning;
$output['channel'] = $tplParam->channal;
if (! empty($tplParam->note)) {
$mdRender = new MdRender(['format' => $this->format]);
$output['note'] = $mdRender->convert($tplParam->note, $this->channel_id);
}
if (isset($isCommunity)) {
$output['isCommunity'] = true;
}
$innerString = "{$output['meaning']}({$output['word']})";
if (! empty($tplParam->other_meaning)) {
$output['meaning2'] = $tplParam->other_meaning;
}
}
$output['innerHtml'] = $innerString;
return $output;
}
private function render_term()
{
$word = $this->get_param($this->param, 'word', 1);
if (str_contains($word, '@')) {
[$wordHead, $tag] = explode('@', $word);
}
if (str_contains($word, '#')) {
[$wordHead, $display] = explode('#', $word);
}
$props = $this->getTermProps($wordHead ?? $word, $tag ?? '');
if (isset($display)) {
$props['meaning'] = $display;
}
$output = $props['word'];
switch ($this->format) {
case 'react':
$output = [
'props' => base64_encode(\json_encode($props)),
'html' => $props['innerHtml'],
'tag' => 'span',
'tpl' => 'term',
];
break;
case 'unity':
$output = [
'props' => base64_encode(\json_encode($props)),
'tpl' => 'term',
];
break;
case 'html':
$no = isset($props['id']) ? '' : 'term_invalid';
$id = isset($props['id']) ? $props['id'] : '';
$output = ' $GLOBALS['note_sn'],
'trigger' => '',
'content' => $content,
];
}
break;
default:
$output = $props['meaning'] ?? $props['word'];
break;
}
return $output;
}
private function render_note()
{
$note = $this->get_param($this->param, 'text', 1);
$trigger = $this->get_param($this->param, 'trigger', 2, '');
// 可选:cite 参数用于在 .sidenote 里追加跳转锚点;citelink 是跳转坐标
// (book-para-start-end),同时挂到角标