فهرست منبع

Merge pull request #2203 from visuddhinanda/laravel

导出支持pandoc
visuddhinanda 1 سال پیش
والد
کامیت
5fb7a11476

+ 5 - 4
app/Console/Commands/ExportArticle.php

@@ -18,7 +18,7 @@ class ExportArticle extends Command
     /**
      * The name and signature of the console command.
      * php artisan export:article 78c22ad3-58e2-4cf0-b979-67783ca3a375 123 --channel=7fea264d-7a26-40f8-bef7-bc95102760fb --format=html
-     * php artisan export:article 4732bcae-fb9d-4db4-b6b7-e8d0aa882f30 1234 --channel=7fea264d-7a26-40f8-bef7-bc95102760fb --anthology=eb9e3f7f-b942-4ca4-bd6f-b7876b59a523 --format=html --token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjE2OTc3Mjg2ODUsImV4cCI6MTcyOTI2NDY4NSwidWlkIjoiYmE1NDYzZjMtNzJkMS00NDEwLTg1OGUtZWFkZDEwODg0NzEzIiwiaWQiOjR9.fiXhnY2LczZ9kKVHV0FfD3AJPZt-uqM5wrDe4EhToVexdd007ebPFYssZefmchfL0mx9nF0rgHSqjNhx4P0yDA
+     * php artisan export:article df6c6609-6fc1-42d0-9ef1-535ef3e702c9 1234 --channel=7fea264d-7a26-40f8-bef7-bc95102760fb --anthology=697c9169-cb9d-4a60-8848-92745e467bab --token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjE2OTc3Mjg2ODUsImV4cCI6MTcyOTI2NDY4NSwidWlkIjoiYmE1NDYzZjMtNzJkMS00NDEwLTg1OGUtZWFkZDEwODg0NzEzIiwiaWQiOjR9.fiXhnY2LczZ9kKVHV0FfD3AJPZt-uqM5wrDe4EhToVexdd007ebPFYssZefmchfL0mx9nF0rgHSqjNhx4P0yDA --format=html
      * @var string
      */
     protected $signature = 'export:article {id} {query_id} {--token=} {--anthology=} {--channel=}  {--origin=false} {--translation=true} {--format=tex} {--debug}';
@@ -125,15 +125,16 @@ class ExportArticle extends Command
     }
 
     private function fetch($articleId){
-        $api = config('app.url') . '/api';
+        $api = config('mint.server.api.bamboo');
         $basicUrl = $api . '/v2/article/';
         $url =  $basicUrl . $articleId;;
         $this->info('http request url='.$url);
         $urlParam = [
                 'mode' => 'read',
-                'format' => 'html',
+                'format' => 'markdown',
                 'anthology'=> $this->option('anthology'),
                 'channel' => $this->option('channel'),
+                'origin' => $this->option('origin'),
         ];
         Log::debug('export article http request',['url'=>$url,'param'=>$urlParam]);
         if($this->option('token')){
@@ -144,7 +145,7 @@ class ExportArticle extends Command
 
         if($response->failed()){
             $this->error('http request error'.$response->json('message'));
-            Log::error('http request error'.$response->json('message'));
+            Log::error('http request error',['error'=>$response->json('message')]);
             return false;
         }
         if(!$response->json('ok')){

+ 8 - 28
app/Console/Commands/ExportChapter.php

@@ -28,7 +28,7 @@ class ExportChapter extends Command
      * php artisan export:chapter 168 915 7fea264d-7a26-40f8-bef7-bc95102760fb 168-915.html --format=html --origin=true
      * @var string
      */
-    protected $signature = 'export:chapter {book} {para} {channel} {query_id} {--token=} {--origin=false} {--translation=true} {--debug} {--format=tex} ';
+    protected $signature = 'export:chapter {book} {para} {channel} {query_id} {--token=} {--origin=false} {--translation=true} {--debug} {--format=markdown} ';
 
     /**
      * The console command description.
@@ -74,23 +74,12 @@ class ExportChapter extends Command
                                         'escape'=>function ($value){
                                             return $value;
                                         }));
-        $tplFile = resource_path("mustache/chapter/".$this->option('format')."/paragraph.".$this->option('format'));
+        $tplFile = resource_path("mustache/chapter/md/paragraph.md");
         $tplParagraph = file_get_contents($tplFile);
 
         MdRender::init();
 
-
-        switch ($this->option('format')) {
-            case 'md':
-                $renderFormat='markdown';
-                break;
-            case 'html':
-                $renderFormat='html';
-                break;
-            default:
-                $renderFormat=$this->option('format');
-                break;
-        }
+        $renderFormat='markdown';
 
         //获取原文channel
         $orgChannelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
@@ -253,20 +242,11 @@ class ExportChapter extends Command
                     }else{
                         $subSessionTitle = $paraData['translations'][0]['content'];
                     }
-                    switch ($this->option('format')) {
-                        case 'tex':
-                            $subStr = array_fill(0,$currLevel,'sub');
-                            $content[] = '\\'. implode('',$subStr) . "section{".$subSessionTitle.'}';
-                            break;
-                        case 'md':
-                            $subStr = array_fill(0,$currLevel,'#');
-                            $content[] = implode('',$subStr) . " ".$subSessionTitle;
-                            break;
-                        case 'html':
-                            $level = $currLevel+2;
-                            $content[] = "<h{$currLevel}>".$subSessionTitle."</h{$currLevel}>";
-                            break;
-                    }
+
+                    //标题
+                    $subStr = array_fill(0,$currLevel,'#');
+                    $content[] = implode('',$subStr) . " ".$subSessionTitle;
+
                 }
                 $content[] = "\n\n";
             }

+ 2 - 2
app/Console/Commands/TestMdRender.php

@@ -12,10 +12,10 @@ class TestMdRender extends Command
 {
     /**
      * The name and signature of the console command.
-     * php artisan test:md.render term unity --driver=str
+     * php artisan test:md.render term --format=unity --driver=str
      * @var string
      */
-    protected $signature = 'test:md.render {item?} {--format=} {--driver=morus}';
+    protected $signature = 'test:md.render {item?} {--format=html} {--driver=morus}';
 
     /**
      * The console command description.

+ 11 - 1
app/Http/Api/MdRender.php

@@ -643,7 +643,17 @@ class MdRender{
                 $output = str_replace('<img src="','<img src="'.config('app.url'),$output);
                 break;
             case 'markdown':
-                $output = $markdownWithTpl;
+                //处理脚注
+                $footnotes = array();
+                if($this->options['footnote'] && isset($GLOBALS['note']) && count($GLOBALS['note'])>0){
+                    foreach ($GLOBALS['note'] as $footnote) {
+                        $footnotes[] = '[^'.$footnote['sn'].']: ' . $footnote['content'];
+                    }
+                    unset($GLOBALS['note']);
+                }
+                //处理图片链接
+                $output = str_replace('/attachments/',config('app.url')."/attachments/",$markdownWithTpl);
+                $output = $output . "\n\n" . implode("\n\n",$footnotes);
                 break;
         }
         return $output;

+ 60 - 8
app/Http/Api/TemplateRender.php

@@ -29,6 +29,20 @@ class TemplateRender{
     protected $lang = 'en';
     protected $langFamily = 'en';
 
+    protected $options = [
+        'mode' => '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'
@@ -59,6 +73,11 @@ class TemplateRender{
             $this->langFamily = explode('-',$lang)[0];
         }
     }
+    public function options($options=[]){
+        foreach ($options as $key => $value) {
+            $this->options[$key] = $value;
+        }
+    }
     public function glossaryKey(){
         return $this->glossaryKey;
     }
@@ -253,6 +272,10 @@ class TemplateRender{
             $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;
             }
@@ -349,6 +372,22 @@ class TemplateRender{
                 }else{
                     $output = $props["word"];
                 }
+                //如果有内容,显示为脚注
+                if(!empty($props["note"])){
+                    if(isset($GLOBALS['note_sn'])){
+                        $GLOBALS['note_sn']++;
+                    }else{
+                        $GLOBALS['note_sn'] = 1;
+                        $GLOBALS['note'] = array();
+                    }
+                    $content = $props["note"];
+                    $output .= '[^'.$GLOBALS['note_sn'].']';
+                    $GLOBALS['note'][] = [
+                        'sn' => $GLOBALS['note_sn'],
+                        'trigger' => '',
+                        'content' => $content,
+                        ];
+                }
                 break;
             default:
                 if(isset($props["meaning"])){
@@ -437,6 +476,7 @@ class TemplateRender{
                     $GLOBALS['note_sn']++;
                 }else{
                     $GLOBALS['note_sn'] = 1;
+                    $GLOBALS['note'] = array();
                 }
                 $content = MdRender::render(
                             $props["note"],
@@ -447,7 +487,13 @@ class TemplateRender{
                             'markdown',
                             'markdown'
                         );
-                $output = '<footnote id="'.$GLOBALS['note_sn'].'">'.$content.'</footnote>';
+                $output = '[^'.$GLOBALS['note_sn'].']';
+                $GLOBALS['note'][] = [
+                    'sn' => $GLOBALS['note_sn'],
+                    'trigger' => $trigger,
+                    'content' => $content,
+                    ];
+                //$output = '<footnote id="'.$GLOBALS['note_sn'].'">'.$content.'</footnote>';
                 break;
             default:
                 $output = '';
@@ -962,16 +1008,22 @@ class TemplateRender{
             case 'markdown':
                 $output = '';
                 if($text === 'both' || $text === 'origin'){
-                    if(isset($props['origin']) && is_array($props['origin'])){
-                        foreach ($props['origin'] as $key => $value) {
-                            $output .= "\n\n". $value['html'];
+                    if($this->options['origin'] === true ||
+                       $this->options['origin'] === 'true'){
+                        if(isset($props['origin']) && is_array($props['origin'])){
+                            foreach ($props['origin'] as $key => $value) {
+                                $output .= $value['html'];
+                            }
                         }
                     }
                 }
                 if($text === 'both' || $text === 'translation'){
-                    if(isset($props['translation']) && is_array($props['translation'])){
-                        foreach ($props['translation'] as $key => $value) {
-                            $output .= "\n\n". $value['html'];
+                    if($this->options['translation']  === true ||
+                       $this->options['translation']  === 'true'){
+                        if(isset($props['translation']) && is_array($props['translation'])){
+                            foreach ($props['translation'] as $key => $value) {
+                                $output .= $value['html'];
+                            }
                         }
                     }
                 }
@@ -980,7 +1032,7 @@ class TemplateRender{
                 $output = '';
                 break;
         }
-        return $output;
+        return trim($output);
     }
 
     private  function render_mermaid(){

+ 165 - 0
app/Http/Controllers/ArticleFtsController.php

@@ -0,0 +1,165 @@
+<?php
+/**
+ * 文章全文搜索
+ */
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+use App\Models\ArticleCollection;
+use App\Models\Article;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Http;
+
+
+class ArticleFtsController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     * http://127.0.0.1:8000/api/v2/article-fts?id=df6c6609-6fc1-42d0-9ef1-535ef3e702c9&anthology=697c9169-cb9d-4a60-8848-92745e467bab&channesl=7fea264d-7a26-40f8-bef7-bc95102760fb
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $pageSize = 10;
+        $pageCurrent = $request->get('from',0);
+
+        $articlesId = [];
+        if(!empty($request->get('anthology'))){
+            //子节点
+            $node = ArticleCollection::where('article_id',$request->get('id'))
+                        ->where('collect_id',$request->get('anthology'))->first();
+            if($node){
+                $nodeList = ArticleCollection::where('collect_id',$request->get('anthology'))
+                                ->where('id','>=',(int)$node->id)
+                                ->orderBy('id')
+                                ->skip($request->get('from',0))
+                                ->get();
+                $result = [];
+                $count = 0;
+                foreach ($nodeList as $curr) {
+                    if($count>0 && $curr->level <= $node->level){
+                        break;
+                    }
+                    $result[] = $curr;
+                }
+                foreach ($result as $key => $value) {
+                    $articlesId[] = $value->article_id;
+                }
+            }
+        }else{
+            $articlesId[] = $request->get('id');
+        }
+        $total = count($articlesId);
+        $channels = explode(',',$request->get('channels'));
+        $output = [];
+        for ($i=$pageCurrent; $i <$pageCurrent+$pageSize ; $i++) {
+            if($i>=$total){
+                break;
+            }
+            $curr = $articlesId[$i];
+            foreach ($channels as $channel) {
+                # code...
+                $article = $this->fetch($curr,$channel);
+                if ($article === false) {
+                    Log::error('fetch fail');
+                }else{
+                    # code...
+                    $content = $article['html'];
+                    if(!empty($request->get('key'))){
+                        if(strpos($content,$request->get('key')) !== false){
+                            $output[] = $article;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $this->ok(['rows'=>$output,
+            'page'=>[
+                'size' => $pageSize,
+                'current' => $pageCurrent,
+                'total' => $total
+            ],]);
+    }
+
+    private function fetch($articleId,$channel,$token=null){
+        try {
+            $api = config('mint.server.api.bamboo');
+            $basicUrl = $api . '/v2/article/';
+            $url =  $basicUrl . $articleId;;
+
+            $urlParam = [
+                    'mode' => 'read',
+                    'format' => 'text',
+                    'channel' => $channel,
+            ];
+            Log::debug('http request',['url'=>$url,'param'=>$urlParam]);
+            if($token){
+                $response = Http::withToken($this->option('token'))->get($url,$urlParam);
+            }else{
+                $response = Http::get($url,$urlParam);
+            }
+
+            if($response->failed()){
+                Log::error('http request error'.$response->json('message'));
+                return false;
+            }
+            if(!$response->json('ok')){
+                return false;
+            }
+            $article = $response->json('data');
+            return $article;
+        }catch (\Throwable $th) {
+            // 处理请求过程中抛出的异常
+            Log::error('fetch',['error'=>$th]);
+            return false;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 2 - 0
app/Http/Middleware/UserOperation.php

@@ -265,12 +265,14 @@ class UserOperation
                 $newFrame->user_id = $user['user_id'];
                 $newFrame->op_start = $currTime - MIN_INTERVAL;
                 $newFrame->op_end = $currTime;
+
                 $newFrame->duration = MIN_INTERVAL;
                 $newFrame->hit = 1;
                 $newFrame->timezone = $client_timezone;
                 $newFrame->save();
                 $this_active_time = MIN_INTERVAL;
             } else {
+                $this_active_time = $currTime - $last->op_end;
                 #修改
                 $last->op_end = $currTime;
                 $last->duration = $currTime - $start_time;

+ 7 - 5
app/Http/Resources/ArticleResource.php

@@ -196,12 +196,14 @@ class ArticleResource extends JsonResource
             $mode = $request->get('mode','read');
             $format = $request->get('format','react');
 
-            $html = MdRender::render($this->content,
-                                     $channels,$query_id,$mode,
-                                     'translation','markdown',$format);
+            $htmlRender = new MdRender([
+                'mode' => $mode,
+                'format'=> $format,
+                'footnote' => true,
+                'origin' => $request->get('origin',true),
+            ]);
             //Log::debug('article render',['content'=>$this->content,'format'=>$format,'html'=>$html]);
-
-            $data["html"] = $html;
+            $data["html"] = $htmlRender->convert($this->content,$channels);
             if(empty($this->summary)){
                 $data["_summary"] = MdRender::render($this->content,
                                                     $channels,$query_id,$mode,

+ 42 - 16
app/Tools/ExportDownload.php

@@ -6,6 +6,9 @@ use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\App;
 
+use Symfony\Component\Process\Process;
+use Symfony\Component\Process\Exception\ProcessFailedException;
+
 use App\Tools\RedisClusters;
 use App\Tools\Export;
 
@@ -85,15 +88,16 @@ class ExportDownload
 
         $tex = array();
 
-        $tplFile = resource_path("mustache/".$type.'/'.$this->format."/main.".$this->format);
+        $_format = 'md';
+        $tplFile = resource_path("mustache/".$type.'/'.$_format."/main.".$_format);
         $tpl = file_get_contents($tplFile);
         $texContent = $m->render($tpl,$bookMeta);
         $tex[] = [
-            'name' => 'main.'.$this->format,
+            'name' => 'main.'.$_format,
             'content' => $texContent
             ];
         foreach ($sections as $key => $section) {
-            $tplFile = resource_path("mustache/".$type.'/'.$this->format."/section.".$this->format);
+            $tplFile = resource_path("mustache/".$type.'/'.$_format."/section.".$_format);
             $tpl = file_get_contents($tplFile);
             $texContent = $m->render($tpl,$section['body']);
             $tex[] = [
@@ -104,7 +108,7 @@ class ExportDownload
 
         Log::debug('footnote start');
         //footnote
-        $tplFile = resource_path("mustache/".$this->format."/footnote.".$this->format);
+        $tplFile = resource_path("mustache/".$_format."/footnote.".$_format);
         if(isset($GLOBALS['note']) &&
             is_array($GLOBALS['note']) &&
             count($GLOBALS['note'])>0 &&
@@ -112,15 +116,12 @@ class ExportDownload
             $tpl = file_get_contents($tplFile);
             $texContent = $m->render($tpl,['footnote'=>$GLOBALS['note']]);
             $tex[] = [
-                'name'=>'footnote.'.$this->format,
+                'name'=>'footnote.'.$_format,
                 'content'=>$texContent
                 ];
         }
         Log::debug('footnote finished');
 
-
-
-
         $this->setStatus(0.95,'export content done. tex count='.count($tex));
         Log::debug('export content done.',['tex_count'=>count($tex)]);
 
@@ -136,7 +137,7 @@ class ExportDownload
                     $this->error($data['code'].'-'.$data['message']);
                 }
                 break;
-            case 'html':
+            default:
                 $file = array();
                 foreach ($tex as $key => $section) {
                     $file[] = $section['content'];
@@ -145,11 +146,36 @@ class ExportDownload
                 break;
         }
 
-        if($this->debug){
-            $dir = "export/{$type}/".$this->format."/".$this->zipFilename."/";
-            $filename = $dir.$outputFilename.'.html';
-            Log::debug('save',['filename'=>$filename]);
-            Storage::disk('local')->put($filename, $fileDate);
+
+        $dir = "tmp/export/{$type}/".$this->format."/";
+        $mdFilename = $dir.$outputFilename.'.md';
+        Storage::disk('local')->put($mdFilename, $fileDate);
+        Log::debug('markdown saved',['filename'=>$mdFilename]);
+        if($this->format === 'markdown'){
+            $filename = $mdFilename;
+        }else{
+            $filename = $dir.$outputFilename.'.'.$this->format;
+
+            Log::debug('tmp saved',['filename'=>$filename]);
+            $absoluteMdPath = Storage::disk('local')->path($mdFilename);
+            $absoluteOutputPath = Storage::disk('local')->path($filename);
+            //$command = "pandoc pandoc1.md --reference-doc tpl.docx -o pandoc1.docx";
+            $command = ['pandoc', $absoluteMdPath, '-o', $absoluteOutputPath];
+            if($this->format === 'docx'){
+                 $tplFile = resource_path("template/docx/paper.docx");
+                 array_push($command,'--reference-doc');
+                 array_push($command,$tplFile);
+            }
+            Log::debug('pandoc start',['command'=>$command,'format'=>$this->format]);
+            $process = new Process($command);
+            $process->run();
+
+            if (!$process->isSuccessful()) {
+                throw new ProcessFailedException($process);
+            }
+
+            echo $process->getOutput();
+            Log::debug('pandoc end',['command'=>$command]);
         }
 
         $zipDir = storage_path('app/export/zip');
@@ -166,8 +192,8 @@ class ExportDownload
         Log::debug('export chapter start zip  file='.$zipFile);
         //zip压缩包里面的文件名
         $realFilename = $this->realFilename.".".$this->format;
-
-        $zipOk = \App\Tools\Tools::zip($zipFile,[$realFilename=>$fileDate]);
+        $fileContent = Storage::disk('local')->get($filename);
+        $zipOk = \App\Tools\Tools::zip($zipFile,[$realFilename=>$fileContent]);
         if(!$zipOk){
             Log::error('export chapter zip fail zip file='.$zipFile);
             $this->setStatus(0.99,'export chapter zip fail');

+ 1 - 1
config/mint.php

@@ -38,7 +38,7 @@ return [
         ],
         'api' => [
             'default' => env('APP_API', "http://localhost:8000/api"),
-            'bamboo' => env('BAMBOO_API_HOST', "http://localhost:8000/api"),
+            'bamboo' => env('BAMBOO_API_HOST', env('APP_URL').'/api'),
         ],
         'assets' => env('ASSETS_SERVER', "localhost:9999"),
 

+ 3 - 0
resources/mustache/article/md/footnote.md

@@ -0,0 +1,3 @@
+[[#footnote]]
+\[[[sn]]\]: [[content]]
+[[/footnote]]

+ 9 - 0
resources/mustache/article/md/glossary.md

@@ -0,0 +1,9 @@
+# glossary
+<div class="glossary">
+[[#pali]]
+<div class="item pali"><span class="head">[[pali]]</span><span class="content">[[meaning]]</span></div>
+[[/pali]]
+[[#meaning]]
+<div class="item meaning"><span class="head">[[meaning]]</span><span  class="content">[[pali]]</span></div>
+[[/meaning]]
+</div>

+ 3 - 0
resources/mustache/article/md/main.md

@@ -0,0 +1,3 @@
+# [[book_title]]
+
+

+ 8 - 0
resources/mustache/article/md/section.md

@@ -0,0 +1,8 @@
+[[#articles]]
+<h[[level]]>[[title]]</h[[level]]>
+
+[[#subtitle]][[subtitle]][[/subtitle]]
+
+[[content]]
+
+[[/articles]]

+ 3 - 0
resources/mustache/chapter/md/footnote.md

@@ -0,0 +1,3 @@
+[[#footnote]]
+\[[[sn]]\]: [[content]]
+[[/footnote]]

+ 15 - 0
resources/mustache/chapter/md/glossary.md

@@ -0,0 +1,15 @@
+<h2>glossary</h2>
+<div class="glossary">
+<h3>Sort by Pali</h3>
+<table>
+[[#pali]]
+<tr class="item pali"><td class="head">[[pali]]</td><td class="content">[[meaning]]</td></tr>
+[[/pali]]
+</table>
+<h3>Sort by Translation</h3>
+<table>
+[[#meaning]]
+<tr class="item meaning"><td class="head">[[meaning]]</td><td  class="content">[[pali]]</td></tr>
+[[/meaning]]
+</table>
+</div>

+ 3 - 0
resources/mustache/chapter/md/main.md

@@ -0,0 +1,3 @@
+# [[book_title]]
+
+

+ 8 - 0
resources/mustache/chapter/md/paragraph.md

@@ -0,0 +1,8 @@
+[[#origin]]
+[[origin]]
+[[/origin]]
+[[#translations]]
+[[content]]
+[[/translations]]
+
+

+ 4 - 0
resources/mustache/chapter/md/section.md

@@ -0,0 +1,4 @@
+## [[title]]
+
+[[content]]
+

+ 7 - 0
resources/mustache/chapter/md/sentence.md

@@ -0,0 +1,7 @@
+[[origin]]
+
+[[#translations]]
+[[content]]
+
+[[/translations]]
+

BIN
resources/template/docx/paper.docx