'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),同时挂到角标