2
0
Эх сурвалжийг харах

Merge branch 'laravel' of https://github.com/iapt-platform/mint into laravel

bhikkhu-kosalla-china 1 жил өмнө
parent
commit
15e54aeb3b
55 өөрчлөгдсөн 1557 нэмэгдсэн , 24858 устгасан
  1. 5 5
      app/Console/Commands/ExportArticle.php
  2. 8 28
      app/Console/Commands/ExportChapter.php
  3. 151 11
      app/Console/Commands/ExportNissaya.php
  4. 16 10
      app/Console/Commands/StatisticsNissaya.php
  5. 144 0
      app/Console/Commands/StatisticsNissayaCover.php
  6. 60 0
      app/Console/Commands/TestAI.php
  7. 34 6
      app/Console/Commands/TestMdRender.php
  8. 10 3
      app/Console/Commands/UpgradeWbwTemplate.php
  9. 7 3
      app/Console/Commands/UpgradeWeekly.php
  10. 24 0
      app/Http/Api/BookTitle.php
  11. 5 1
      app/Http/Api/ChannelApi.php
  12. 64 15
      app/Http/Api/MdRender.php
  13. 292 36
      app/Http/Api/TemplateRender.php
  14. 2 0
      app/Http/Api/quote-my.csv
  15. 101 0
      app/Http/Controllers/AiTranslateController.php
  16. 165 0
      app/Http/Controllers/ArticleFtsController.php
  17. 14 4
      app/Http/Controllers/ArticleMapController.php
  18. 1 0
      app/Http/Controllers/CorpusController.php
  19. 75 0
      app/Http/Controllers/EditableSentenceController.php
  20. 13 6
      app/Http/Controllers/NissayaCardController.php
  21. 66 0
      app/Http/Controllers/NissayaCoverController.php
  22. 1 1
      app/Http/Controllers/TermVocabularyController.php
  23. 21 1
      app/Http/Controllers/WbwSentenceController.php
  24. 2 0
      app/Http/Middleware/UserOperation.php
  25. 1 0
      app/Http/Resources/ArticleMapResource.php
  26. 11 4
      app/Http/Resources/ArticleResource.php
  27. 43 17
      app/Tools/ExportDownload.php
  28. 1 1
      config/mint.php
  29. 2 1
      database/export/sentence.sql
  30. 34 0
      database/migrations/2024_07_23_032240_add_type2_in_channels.php
  31. 37 0
      database/migrations/2024_07_23_033220_migrate_type_in_channels.php
  32. 36 0
      database/migrations/2024_07_23_033823_change_type2_to_type_in_channels.php
  33. 0 4
      public/app/dbadmin/.gitignore
  34. 0 82
      public/app/dbadmin/3rd/config.sample.php
  35. 0 6090
      public/app/dbadmin/3rd/pla.php
  36. 0 82
      public/app/dbadmin/palicanon/config.sample.php
  37. 0 6090
      public/app/dbadmin/palicanon/pla.php
  38. 0 82
      public/app/dbadmin/system/config.sample.php
  39. 0 6090
      public/app/dbadmin/system/pla.php
  40. 0 82
      public/app/dbadmin/user/config.sample.php
  41. 0 6090
      public/app/dbadmin/user/pla.php
  42. 0 13
      public/app/studio/file_save.php
  43. 39 0
      resources/mustache/article/html/main.html
  44. 3 0
      resources/mustache/article/md/footnote.md
  45. 9 0
      resources/mustache/article/md/glossary.md
  46. 3 0
      resources/mustache/article/md/main.md
  47. 8 0
      resources/mustache/article/md/section.md
  48. 3 0
      resources/mustache/chapter/md/footnote.md
  49. 15 0
      resources/mustache/chapter/md/glossary.md
  50. 3 0
      resources/mustache/chapter/md/main.md
  51. 8 0
      resources/mustache/chapter/md/paragraph.md
  52. 4 0
      resources/mustache/chapter/md/section.md
  53. 7 0
      resources/mustache/chapter/md/sentence.md
  54. BIN
      resources/template/docx/paper.docx
  55. 9 0
      routes/api.php

+ 5 - 5
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}';
@@ -66,7 +66,6 @@ class ExportArticle extends Command
                                             return $value;
                                         }));
 
-
         $sections = array();
         $articles = array();
 
@@ -132,11 +131,12 @@ class ExportArticle extends Command
         $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')){
             $response = Http::withToken($this->option('token'))->get($url,$urlParam);
         }else{
@@ -145,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";
             }

+ 151 - 11
app/Console/Commands/ExportNissaya.php

@@ -6,12 +6,85 @@ use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\Channel;
 use App\Models\Sentence;
+use App\Models\NissayaEnding;
+use App\Models\UserDict;
+use App\Http\Api\DictApi;
+
+class SuttaType
+{
+    public static function types(){
+        return     [
+            'mula'=>[
+                69,70,71,72,73,74,
+                75,76,77,78,79,80,
+                81,82,83,84,85,86,
+                87,88,89,90,91,92,
+                93,94,95,143,144,145,
+                146,147,148,149,150,151,
+                152,153,154,155,156,157,
+                158,159,160,161,162,163,
+                164,165,166,167,168,169,
+                170,171,213,214,215,216,217,
+            ],
+            'atthakatha' => [
+                64,65,96,97,98,99,
+                100,101,102,103,104,105,
+                106,107,108,109,110,111,
+                112,113,114,115,116,117,
+                118,119,120,121,122,123,
+                124,125,126,127,128,129,
+                130,131,132,133,134,135,
+                136,137,138,139,140,141,142,
+            ],
+            'tika' => [
+                66,67,68,172,173,174,
+                175,176,177,178,179,180,
+                181,182,183,184,185,186,
+                187,188,189,190,191,192,
+                193,194,195,196,197,198,
+                199,200,201,202,203,204,
+                205,206,207,208,209,210,211,212,
+            ],
+            'vinaya' => [138,139,140,141,142,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,],
+            'sutta' => [
+                82,83,84,85,86,
+                87,88,89,90,91,92,93,
+                94,95,99,100,101,102,
+                103,104,105,106,107,108,
+                109,110,111,112,113,114,
+                115,116,117,118,119,120,
+                121,122,123,124,125,126,
+                127,128,129,130,131,132,
+                133,134,135,136,137,143,
+                144,145,146,147,148,149,
+                150,151,152,153,154,155,
+                156,157,158,159,160,161,
+                162,163,164,165,166,167,
+                168,169,170,171,181,182,
+                183,184,185,186,187,188,
+                189,190,191,192,193,194,
+                195,196,197,198,199,
+            ],
+            'abhidhamma' => [69,70,71,72,73,74,75,76,77,78,79,80,81,96,97,98,172,173,174,175,176,177,178,179,180,],
+        ];
+    }
+
+    public static function getTypeByBook($bookId){
+        $types = [];
+        foreach (SuttaType::types() as $type => $books) {
+            if(in_array($bookId,$books)){
+                $types[] = $type;
+            }
+        }
+        return $types;
+    }
+}
 
 class ExportNissaya extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan export:nissaya
      * @var string
      */
     protected $signature = 'export:nissaya';
@@ -23,7 +96,7 @@ class ExportNissaya extends Command
      *
      * @var string
      */
-    protected $description = 'Command description';
+    protected $description = '导出nissaya统计数据';
 
     /**
      * Create a new command instance.
@@ -45,35 +118,102 @@ class ExportNissaya extends Command
         if(\App\Tools\Tools::isStop()){
             return 0;
         }
-        $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
-        $channels = [];
-        foreach ($nissaya_channel as $key => $value) {
-            # code...
-            $channels[] = $value->uid;
+        $nissaya_channels = Channel::where('type','nissaya')
+                            ->where('lang','my')
+                            ->select('uid')->get();
+        $this->info('channel:'.count($nissaya_channels));
+        //system regular
+        $dict_id = DictApi::getSysDict('system_regular');
+        if(!$dict_id){
+            $this->error('没有找到 system_regular 字典');
+            return 1;
+        }else{
+            $this->info("system_regular :{$dict_id}");
         }
-        $this->info('channel:'.count($channels));
+
+        //获取缅文语尾表
+        $nissayaEndings = NissayaEnding::select('ending')->groupBy('ending')->get();
+        $endings = [];
+        $maxLen = 0;
+        foreach ($nissayaEndings as $key => $ending) {
+            $endings[] = $ending->ending;
+            if(mb_strlen($ending->ending,'UTF-8')>$maxLen){
+                $maxLen = mb_strlen($ending->ending,'UTF-8');
+            }
+        }
+        $this->info(count($endings).' ending');
+
         $filename = "public/export/nissaya.csv";
         Storage::disk('local')->put($filename, "");
         $file = fopen(storage_path("app/$filename"),"w");
-        $bar = $this->output->createProgressBar(Sentence::whereIn('channel_uid',$channels)->count());
-        foreach (Sentence::whereIn('channel_uid',$channels)->select('content')->cursor() as $sent) {
+        $bar = $this->output->createProgressBar(Sentence::whereIn('channel_uid',$nissaya_channels)->count());
+        foreach (Sentence::whereIn('channel_uid',$nissaya_channels)->select(['content','book_id'])->cursor() as $sent) {
             $lines = explode("\n",$sent->content);
             foreach ($lines as $key => $line) {
                 # code...
                 if(substr_count(trim($line),'=') === 1){
                     $nissaya_str = explode('=',$line);
                     $pali = $this->my2en($nissaya_str[0]);
-                    fputcsv($file,[$pali,$nissaya_str[1]]);
+                    $types = SuttaType::getTypeByBook($sent->book_id);
+                    $strTypes = implode(",",$types);
+                    //拆分
+                    $factors = UserDict::where('dict_id',$dict_id)->where('word',$pali)->value('factors');
+                    $factors = explode('+',$factors);
+                    if(count($factors)>1){
+                        $paliEnding = end($factors);
+                    }else{
+                        $paliEnding = '';
+                    }
+                    $nissaya_my = trim($nissaya_str[1]);
+                    $mEnding1 = $this->matchEnding($nissaya_my,$endings,$maxLen);
+                    if(!empty($paliEnding) && !empty($mEnding1[1])){
+                        $mixed = $paliEnding.$mEnding1[1];
+                        fputcsv($file,[$strTypes, $pali,$paliEnding,$nissaya_my,$mEnding1[1],$mixed]);
+                    }
+                    $mEnding2= ['',''];
+                    if(!empty($mEnding1[1])){
+                        $mEnding2 = $this->matchEnding($mEnding1[0],$endings,$maxLen);
+                        if(!empty($paliEnding) && !empty($mEnding2[1])){
+                            $mixed = $paliEnding.$mEnding2[1];
+                            fputcsv($file,[$strTypes, $pali,$paliEnding,$nissaya_my,$mEnding2[1],$mixed]);
+                        }
+                    }
+                    $mEnding3= ['',''];
+                    if(!empty($mEnding2[1])){
+                        $mEnding3 = $this->matchEnding($mEnding2[0],$endings,$maxLen);
+                        if(!empty($paliEnding) && !empty($mEnding3[1])){
+                            $mixed = $paliEnding.$mEnding3[1];
+                            fputcsv($file,[$strTypes, $pali,$paliEnding,$nissaya_my,$mEnding3[1],$mixed]);
+                        }
+                    }
+
+                    //fputcsv($file,[$strTypes, $pali,$paliEnding,$nissaya_my,$mEnding1[1],$mEnding2[1],$mEnding3[1]]);
                 }
             }
             $bar->advance();
         }
         fclose($file);
         $bar->finish();
+        $this->info('done');
+        $this->info($filename);
         return 0;
     }
 
     public function my2en($my){
         return str_replace($this->my,$this->en,$my);
     }
+
+    private function matchEnding($needle,$endings,$maxLen){
+        $needle = trim($needle);
+        if(mb_substr($needle,-1,1,'UTF-8') === '။'){
+            $needle = mb_substr($needle,0,-1);
+        }
+        for ($i=1; $i <= $maxLen ; $i++) {
+            $mEnding = mb_substr($needle,-$i);
+            if(in_array($mEnding,$endings)){
+                return [mb_substr($needle,0,mb_strlen($needle,'UTF-8')-$i),$mEnding];
+            }
+        }
+        return [$needle,''];
+    }
 }

+ 16 - 10
app/Console/Commands/StatisticsNissaya.php

@@ -12,7 +12,7 @@ class StatisticsNissaya extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan statistics:nissaya
      * @var string
      */
     protected $signature = 'statistics:nissaya';
@@ -44,13 +44,15 @@ class StatisticsNissaya extends Command
         if(\App\Tools\Tools::isStop()){
             return 0;
         }
-        $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
-        $this->info('channel:'.count($nissaya_channel));
-        $maxDay = 360;
+        $nissaya_channels = Channel::where('type','nissaya')
+                            ->where('lang','my')
+                            ->select('uid')->get();
+        $this->info('channel:'.count($nissaya_channels));
         $file = "public/statistics/nissaya-monthly.csv";
+
         Storage::disk('local')->put($file, "");
         #按月获取数据
-        $firstDay = Sentence::whereIn('channel_uid',$nissaya_channel)
+        $firstDay = Sentence::whereIn('channel_uid',$nissaya_channels)
                             ->orderBy('created_at')
                             ->select('created_at')
                             ->first();
@@ -59,26 +61,30 @@ class StatisticsNissaya extends Command
         $now = Carbon::now();
         $current = $firstMonth;
         $sumStrlen = 0;
+        $sumCount = 0;
         while ($current <= $now) {
             # code...
             $start = Carbon::create($current)->startOfMonth();
             $end = Carbon::create($current)->endOfMonth();
             $date = $current->format('Y-m');
-            $strlen = Sentence::whereIn('channel_uid',$nissaya_channel)
+            $table = Sentence::whereIn('channel_uid',$nissaya_channels)
                               ->whereDate('created_at','>=',$start)
-                              ->whereDate('created_at','<=',$end)
-                              ->sum('strlen');
+                              ->whereDate('created_at','<=',$end);
+            $strlen =  $table->sum('strlen');
             $sumStrlen += $strlen;
-            $editor = Sentence::whereIn('channel_uid',$nissaya_channel)
+            $count = $table->count();
+            $sumCount += $count;
+            $editor = Sentence::whereIn('channel_uid',$nissaya_channels)
                               ->whereDate('created_at','>=',$start)
                               ->whereDate('created_at','<=',$end)
                               ->groupBy('editor_uid')
                               ->select('editor_uid')->get();
-            $info = $date.','.$strlen.','.$sumStrlen.','.count($editor);
+            $info = "{$date},{$strlen},{$sumStrlen},{$count},{$sumCount},".count($editor);
             $this->info($info);
             Storage::disk('local')->append($file, $info);
             $current->addMonth(1);
         }
+        $this->info('path='.$file);
         return 0;
     }
 }

+ 144 - 0
app/Console/Commands/StatisticsNissayaCover.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Channel;
+use App\Models\Sentence;
+use App\Models\PaliSentence;
+use App\Tools\RedisClusters;
+
+class StatisticsNissayaCover extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan statistics:nissaya.cover
+     * @var string
+     */
+    protected $signature = 'statistics:nissaya.cover';
+    protected $types = [
+        'mula'=>[
+            69,70,71,72,73,74,
+            75,76,77,78,79,80,
+            81,82,83,84,85,86,
+            87,88,89,90,91,92,
+            93,94,95,143,144,145,
+            146,147,148,149,150,151,
+            152,153,154,155,156,157,
+            158,159,160,161,162,163,
+            164,165,166,167,168,169,
+            170,171,213,214,215,216,217,
+        ],
+        'atthakatha' => [
+            64,65,96,97,98,99,
+            100,101,102,103,104,105,
+            106,107,108,109,110,111,
+            112,113,114,115,116,117,
+            118,119,120,121,122,123,
+            124,125,126,127,128,129,
+            130,131,132,133,134,135,
+            136,137,138,139,140,141,142,
+        ],
+        'tika' => [
+            66,67,68,172,173,174,
+            175,176,177,178,179,180,
+            181,182,183,184,185,186,
+            187,188,189,190,191,192,
+            193,194,195,196,197,198,
+            199,200,201,202,203,204,
+            205,206,207,208,209,210,211,212,
+        ],
+        'vinaya' => [138,139,140,141,142,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,],
+        'sutta' => [
+            82,83,84,85,86,
+            87,88,89,90,91,92,93,
+            94,95,99,100,101,102,
+            103,104,105,106,107,108,
+            109,110,111,112,113,114,
+            115,116,117,118,119,120,
+            121,122,123,124,125,126,
+            127,128,129,130,131,132,
+            133,134,135,136,137,143,
+            144,145,146,147,148,149,
+            150,151,152,153,154,155,
+            156,157,158,159,160,161,
+            162,163,164,165,166,167,
+            168,169,170,171,181,182,
+            183,184,185,186,187,188,
+            189,190,191,192,193,194,
+            195,196,197,198,199,
+        ],
+        'abhidhamma' => [69,70,71,72,73,74,75,76,77,78,79,80,81,96,97,98,172,173,174,175,176,177,178,179,180,],
+    ];
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '统计nissaya覆盖度';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $nissaya_channels = Channel::where('type','nissaya')
+                                ->where('lang','my')
+                                ->select('uid')->get();
+        $this->info('channel:'.count($nissaya_channels));
+        $output = [];
+        foreach ($this->types as $type => $books) {
+            # code...
+            $pali = PaliSentence::whereIn('book',$books)->sum('length');
+            $nissayaSentences = Sentence::whereIn('channel_uid',$nissaya_channels)
+                                ->whereIn('book_id',$books)
+                                ->groupBy(['book_id','paragraph','word_start','word_end'])
+                                ->select(['book_id','paragraph','word_start','word_end'])
+                                ->get();
+            $sentences = [];
+            $final = 0;
+            $this->info($type . count($nissayaSentences). " sentences");
+            if(count($nissayaSentences)>0){
+                $count = 0;
+                foreach ($nissayaSentences as  $value) {
+                    $sentences[] = [
+                        $value->book_id,
+                        $value->paragraph,
+                        $value->word_start,
+                        $value->word_end,
+                    ];
+                    if($count % 100 === 0 ){
+                        $final += PaliSentence::whereIns(['book','paragraph','word_begin','word_end'],$sentences)
+                                        ->sum('length');
+                        $sentences = [];
+                        $percent = intval($count * 100 / count($nissayaSentences));
+                        $this->info("[{$percent}] {$final}");
+                    }
+                    $count++;
+                }
+
+            }
+
+            $this->info($type . '=' . $pali . '=' . $final);
+            $output[] = ['type'=>$type,'total'=>$pali,'final'=>$final];
+
+        }
+        RedisClusters::put('/statistics/nissaya/cover',$output,48*3600);
+        return 0;
+    }
+}

+ 60 - 0
app/Console/Commands/TestAI.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class TestAI extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:ai';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $url = 'https://api.moonshot.cn/v1/chat/completions';
+        $param = [
+                "model" => "moonshot-v1-8k",
+                "messages" => [
+                    ["role" => "system","content" => "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"],
+                    ["role" => "user","content" => "你好,我叫李雷,1+1等于多少?"],
+                ],
+                "temperature" => 0.3,
+        ];
+        $response = Http::withToken('sk-kwjHIMh3PoWwUwQyKdT3KHvNe8Es19SUiujGrxtH09uDQCui')
+                        ->post($url,$param);
+        if($response->failed()){
+            $this->error('http request error'.$response->json('message'));
+        }else{
+            $this->info(json_encode($response->json()));
+        }
+        return 0;
+    }
+}

+ 34 - 6
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.
@@ -117,6 +117,31 @@ class TestMdRender extends Command
         title=第一章 戒律概说(Vinaya)|
         style=modal}}
         md;
+
+        $data['footnote'] = <<<md
+        # title
+        content `note content` `note2 content`
+        md;
+
+        $data['paragraph'] = <<<md
+        # title
+        content
+
+        {{168-916-10-37}}
+        {{168-916-10-37}}
+
+        the end
+        md;
+
+        $data['img'] = <<<md
+        # title
+        content
+
+        ![aaa](/images/aaa.jpg)
+
+        the end
+        md;
+
         $data['empty'] = '';
 
         Markdown::driver($this->option('driver'));
@@ -134,10 +159,13 @@ class TestMdRender extends Command
                 if(!empty($_item) && $key !==$_item){
                     continue;
                 }
-                echo MdRender::render($value,
-                                    ['00ae2c48-c204-4082-ae79-79ba2740d506'],
-                                    null,'read','translation',
-                                    $contentType="markdown",$format);
+                $mdRender = new MdRender([
+                    'format'=>$format,
+                    'footnote'=>true,
+                    'paragraph'=>true,
+                ]);
+                $output = $mdRender->convert($value,['00ae2c48-c204-4082-ae79-79ba2740d506']);
+                echo $output;
             }
         }
         return 0;

+ 10 - 3
app/Console/Commands/UpgradeWbwTemplate.php

@@ -13,10 +13,10 @@ class UpgradeWbwTemplate extends Command
 {
     /**
      * The name and signature of the console command.
-     * php artisan upgrade:wbw.template 107
+     * php artisan upgrade:wbw.template  129 509
      * @var string
      */
-    protected $signature = 'upgrade:wbw.template {book?} {para?}';
+    protected $signature = 'upgrade:wbw.template {book?} {para?} {--debug=false}';
 
     /**
      * The console command description.
@@ -59,6 +59,7 @@ class UpgradeWbwTemplate extends Command
             $this->error('no channel');
             return 1;
         }
+        $this->info('channel id='.$channelId);
 		$pali = $pali->select('book','paragraph','word_begin','word_end')->cursor();
 		foreach ($pali as $value) {
 			# code...
@@ -70,6 +71,10 @@ class UpgradeWbwTemplate extends Command
 								->orderBy('wid','asc')
 								->get();
 			$sent = '';
+            $sentId = $value->book.'-'.$value->paragraph.'-'.$value->word_begin.'-'.$value->word_end;
+            if(count($words)===0){
+                $this->error('no word data in'.$sentId);
+            }
 			foreach ($words as $wbw_word) {
                 # code...
                 $type = $wbw_word->type=='?'? '':$wbw_word->type;
@@ -96,7 +101,9 @@ class UpgradeWbwTemplate extends Command
 
             }
             $sent = \json_encode($wbwContent,JSON_UNESCAPED_UNICODE);
-
+            if($this->option('debug')==='true'){
+                $this->info($sentId.'='.$sent);
+            }
 			$newRow = Sentence::firstOrNew(
 				[
 					"book_id" => $value->book,

+ 7 - 3
app/Console/Commands/UpgradeWeekly.php

@@ -40,21 +40,25 @@ class UpgradeWeekly extends Command
         if(\App\Tools\Tools::isStop()){
             return 0;
         }
+        $currTime = time();
         #译文进度
         $this->call('upgrade:progress');
         $time = time()-$currTime;
-        $message .= "progress:{$time}; ";
+        $message = "progress time:{$time}; ";
+        $this->info($message);
         $currTime = time();
 
         $this->call('upgrade:progress.chapter');
         $time = time()-$currTime;
-        $message .= "progress.chapter:{$time}; ";
+        $message = "progress.chapter time:{$time}; ";
+        $this->info($message);
         $currTime = time();
 
         # 逐词译数据库分析
         $this->call('upgrade:wbw.analyses');
         $time = time()-$currTime;
-        $message .= "wbw.analyses:{$time}; ";
+        $message = "wbw.analyses:{$time}; ";
+        $this->info($message);
 
         # 段落更新图
         $this->call('upgrade:chapter.dynamic');

+ 24 - 0
app/Http/Api/BookTitle.php

@@ -11,6 +11,12 @@ class BookTitle{
     "bookname" => "dhammasaṅgaṇīpāḷi",
     "volume" => 0
   ],
+  [
+    "title1" => "abhi,1",
+    "title2" => "အဘိ၊၁",
+    "bookname" => "dhammasaṅgaṇīpāḷi",
+    "volume" => 0
+  ],
   [
     "title1" => "abhi,ka",
     "title2" => "အဘိ၊က",
@@ -47,6 +53,12 @@ class BookTitle{
     "bookname" => "vibhaṅgapāḷi",
     "volume" => 0
   ],
+  [
+    "title1" => "abhi,vi",
+    "title2" => "အဘိ၊၂",
+    "bookname" => "vibhaṅgapāḷi",
+    "volume" => 0
+  ],
   [
     "title1" => "aṃ,1",
     "title2" => "အံ၊၁",
@@ -714,6 +726,18 @@ class BookTitle{
     "bookname" => "visuddhimagga-mahāṭīkā",
     "volume" => 2
   ],
+  [
+    "title1" => "visuddhi,ṭī,1",
+    "title2" => "မဟာဋီ၊၁",
+    "bookname" => "visuddhimagga-mahāṭīkā",
+    "volume" => 1
+  ],
+  [
+    "title1" => "visuddhi,ṭī,2",
+    "title2" => "မဟာဋီ၊၂",
+    "bookname" => "visuddhimagga-mahāṭīkā",
+    "volume" => 2
+  ],
   [
     "title1" => "yamaka,1",
     "title2" => "ယမက၊၁",

+ 5 - 1
app/Http/Api/ChannelApi.php

@@ -83,7 +83,11 @@ class ChannelApi{
         }
         return $output;
     }
-
+    public static function canManageByUser($channelId,$userUuid){
+        $isOwner = Channel::where('owner_uid', $userUuid)
+                    ->where('uid', $channelId)->exists();
+        return $isOwner;
+    }
     public static function getSysChannel($channel_name,$fallback=""){
         $channel = Channel::where('name',$channel_name)
                     ->where('owner_uid',config("mint.admin.root_uuid"))

+ 64 - 15
app/Http/Api/MdRender.php

@@ -28,6 +28,8 @@ class MdRender{
         'debug'=>[],
         'studioId'=>null,
         'lang'=>'zh-Hans',
+        'footnote'=>false,
+        'paragraph'=>false,
         ];
 
     public function __construct($options=[])
@@ -37,6 +39,31 @@ class MdRender{
         }
     }
 
+    /**
+     * 将句子模版组成的段落复制一份,为了实现巴汉逐段对读
+     */
+    private function preprocessingForParagraph($input){
+        if(!$this->options['paragraph']){
+            return $input;
+        }
+        $paragraphs = explode("\n\n",$input);
+        $output = [];
+        foreach ($paragraphs as $key => $paragraph) {
+            # 判断是否是纯粹的句子模版
+            $pattern = "/\{\{sent\|id=([0-9].+?)\}\}/";
+            $replacement = '';
+            $space = preg_replace($pattern,$replacement,$paragraph);
+            if(empty(trim($space))){
+                $output[] = str_replace('}}','|text=origin}}',$paragraph);
+                $output[] = str_replace('}}','|text=translation}}',$paragraph);
+            }else{
+                $output[] = $paragraph;
+            }
+        }
+
+        return implode("\n\n",$output);
+    }
+
     /**
      * 按照{{}}把字符串切分成三个部分。模版之前的,模版,和模版之后的
      */
@@ -99,7 +126,7 @@ class MdRender{
 
     private function wiki2xml(string $wiki,$channelId=[]):string{
         /**
-         * 把模版转换为xml
+         * 渲染markdown里面的模版
          */
         $remain = $wiki;
         $buffer = array();
@@ -216,9 +243,8 @@ class MdRender{
             Log::error($xml);
             return "<span>xml解析错误</span>";
         }
-         */
+        */
 
-        //$tpl_list = $dom->xpath('//MdTpl');
         $tpl_list = $doc->getElementsByTagName('dfn');
 
         foreach ($tpl_list as $key => $tpl) {
@@ -276,7 +302,7 @@ class MdRender{
                                         $this->options['debug'],
                                         $this->options['lang'],
                                     );
-
+            $tplRender->options($this->options);
             $tplProps = $tplRender->render($tpl_name);
             if($this->options['format']==='react' && $tplProps){
                 $props = $doc->createAttribute("props");
@@ -315,8 +341,7 @@ class MdRender{
                     return '';
                 }
                 break;
-            case 'text':
-            case 'simple':
+            case 'tex':
                 if(isset($tplProps)){
                     if(is_array($tplProps)){
                         return '';
@@ -328,7 +353,7 @@ class MdRender{
                     return '';
                 }
                 break;
-            case 'tex':
+            default: /**text simple markdown */
                 if(isset($tplProps)){
                     if(is_array($tplProps)){
                         return '';
@@ -340,13 +365,12 @@ class MdRender{
                     return '';
                 }
                 break;
-            default:
-                return '';
-                break;
         }
     }
 
-
+    /**
+     * 将markdown文件中的模版转换为标准的wiki模版
+     */
     private function markdown2wiki(string $markdown): string{
         //$markdown = mb_convert_encoding($markdown,'UTF-8','UTF-8');
         $markdown = iconv('UTF-8','UTF-8//IGNORE',$markdown);
@@ -447,7 +471,7 @@ class MdRender{
 
         #替换句子模版
         $pattern = "/\{\{([0-9].+?)\}\}/";
-        $replacement = '{{sent|$1}}';
+        $replacement = '{{sent|id=$1}}';
         $markdown = preg_replace($pattern,$replacement,$markdown);
 
         /**
@@ -570,11 +594,12 @@ class MdRender{
             }
         }
         $wiki = $this->markdown2wiki($markdown);
-        $html = $this->wiki2xml($wiki,$channelId);
+        $wiki = $this->preprocessingForParagraph($wiki);
+        $markdownWithTpl = $this->wiki2xml($wiki,$channelId);
         if(!is_null($queryId)){
-            $html = $this->xmlQueryId($html, $queryId);
+            $html = $this->xmlQueryId($markdownWithTpl, $queryId);
         }
-        $html = $this->markdownToHtml($html);
+        $html = $this->markdownToHtml($markdownWithTpl);
 
         //后期处理
         $output = '';
@@ -605,6 +630,30 @@ class MdRender{
                 break;
             case 'html':
                 $output = htmlspecialchars_decode($html,ENT_QUOTES);
+                //处理脚注
+                if($this->options['footnote'] && isset($GLOBALS['note']) && count($GLOBALS['note'])>0){
+                    $output .= '<div><h1>endnote</h1>';
+                    foreach ($GLOBALS['note'] as $footnote) {
+                        $output .= '<p><a name="footnote-'.$footnote['sn'].'">['.$footnote['sn'].']</a> '.$footnote['content'].'</p>';
+                    }
+                    $output .= '</div>';
+                    unset($GLOBALS['note']);
+                }
+                //处理图片链接
+                $output = str_replace('<img src="','<img src="'.config('app.url'),$output);
+                break;
+            case 'markdown':
+                //处理脚注
+                $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;

+ 292 - 36
app/Http/Api/TemplateRender.php

@@ -11,6 +11,7 @@ use App\Models\PaliText;
 use App\Models\Channel;
 use App\Models\PageNumber;
 use App\Models\Discussion;
+use App\Models\BookTitle as BookSeries;
 
 use App\Http\Controllers\CorpusController;
 
@@ -29,13 +30,27 @@ 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'
      * string $format  'react' | 'text' | 'tex' | 'unity'
      * @return void
      */
-    public function __construct($param, $channelInfo, $mode,$format='react',$studioId,$debug=[],$lang='zh-Hans')
+    public function __construct($param, $channelInfo, $mode,$format='react',$studioId='',$debug=[],$lang='zh-Hans')
     {
         $this->param = $param;
         foreach ($channelInfo as $value) {
@@ -59,6 +74,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;
     }
@@ -118,6 +138,9 @@ class TemplateRender{
             case 'g':
                 $result = $this->render_grammar_lookup();
                 break;
+            case 'ref':
+                $result = $this->render_ref();
+                break;
             default:
                 # code...
                 $result = [
@@ -253,6 +276,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;
             }
@@ -270,7 +297,6 @@ class TemplateRender{
 
         $props = $this->getTermProps($word,'');
 
-        //$key = "/term/{$channelId}/{$word}";
         $output = $props['word'];
         switch ($this->format) {
             case 'react':
@@ -337,6 +363,35 @@ class TemplateRender{
                     $output = $props["word"];
                 }
                 break;
+            case 'markdown':
+                if(isset($props["meaning"])){
+                    $key = 'term-'.$props["word"];
+                    if(isset($GLOBALS[$key])){
+                        $output = $props["meaning"];
+                    }else{
+                        $GLOBALS[$key] = 1;
+                        $output = $props["meaning"].'('.$props["word"].')';
+                    }
+                }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"])){
                     $output = $props["meaning"];
@@ -358,14 +413,15 @@ class TemplateRender{
             $innerString = $props["trigger"];
         }
         if($this->format==='unity'){
-            $props["note"] = MdRender::render($props["note"],
-                                $this->channel_id,
-                                null,
-                                'read',
-                                'translation',
-                                'markdown',
-                                'unity'
-                                );
+            $props["note"] = MdRender::render(
+                $props["note"],
+                $this->channel_id,
+                null,
+                'read',
+                'translation',
+                'markdown',
+                'unity'
+                );
         }
         $output = $note;
         switch ($this->format) {
@@ -391,7 +447,7 @@ class TemplateRender{
                     $GLOBALS['note'] = array();
                 }
                 $GLOBALS['note'][] = [
-                        'sn' => 1,
+                        'sn' => $GLOBALS['note_sn'],
                         'trigger' => $trigger,
                         'content' => MdRender::render($props["note"],
                                         $this->channel_id,
@@ -419,6 +475,30 @@ class TemplateRender{
             case 'simple':
                 $output = '';
                 break;
+            case 'markdown':
+                if(isset($GLOBALS['note_sn'])){
+                    $GLOBALS['note_sn']++;
+                }else{
+                    $GLOBALS['note_sn'] = 1;
+                    $GLOBALS['note'] = array();
+                }
+                $content = MdRender::render(
+                            $props["note"],
+                            $this->channel_id,
+                            null,
+                            'read',
+                            'translation',
+                            'markdown',
+                            'markdown'
+                        );
+                $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 = '';
                 break;
@@ -508,16 +588,19 @@ class TemplateRender{
         $type = $this->get_param($this->param,"type",1);
         $id = $this->get_param($this->param,"id",2);
         $title = $this->get_param($this->param,"title",3);
-        $channel = $this->get_param($this->param,"channel",4,$this->channel_id[0]);
+        $channel = $this->get_param($this->param,"channel",4);
         $style = $this->get_param($this->param,"style",5);
         $book = $this->get_param($this->param,"book",6);
         $paragraphs = $this->get_param($this->param,"paragraphs",7);
+        $anthology = $this->get_param($this->param,"anthology",8);
         $props = [
                     "type" => $type,
                     "id" => $id,
-                    "channel" => $channel,
                     'style' => $style,
                 ];
+        if(!empty($channel)){
+            $props['channel'] = $channel;
+        }
         if(!empty($title)){
             $props['title'] = $title;
         }
@@ -527,6 +610,12 @@ class TemplateRender{
         if(!empty($paragraphs)){
             $props['paragraphs'] = $paragraphs;
         }
+        if(!empty($anthology)){
+            $props['anthology'] = $anthology;
+        }
+        if(is_array($this->channel_id)){
+            $props['parentChannels'] = $this->channel_id;
+        }
         switch ($this->format) {
             case 'react':
                 $output = [
@@ -668,10 +757,23 @@ class TemplateRender{
             }
         }else if($title){
             //没有书号用title查询
-            $tmpTitle = explode('။',$title);
-            if(count($tmpTitle)>1){
-                $tmpBookTitle = $tmpTitle[0];
-                $tmpBookPage = $tmpTitle[1];
+            //$tmpTitle = explode('။',$title);
+            for ($i=mb_strlen($title,'UTF-8'); $i > 0 ; $i--) {
+                $mTitle = mb_substr($title,0,$i);
+                $has = array_search($mTitle, array_column(BookTitle::my(), 'title2'));
+                Log::debug('run',['title'=>$mTitle,'has'=>$has]);
+                if($has !== false){
+                    $tmpBookTitle = $mTitle;
+                    $tmpBookPage = mb_substr($title,$i);
+                    $tmpBookPage = $this->mb_trim($tmpBookPage,'၊။');
+                    break;
+                }
+            }
+
+            if(isset($tmpBookTitle)){
+                Log::debug('book title found',['title'=>$tmpBookTitle,'page'=>$tmpBookPage]);
+                //$tmpBookTitle = $tmpTitle[0];
+                //$tmpBookPage = $tmpTitle[1];
                 $tmpBookPage = (int)str_replace(
                                 ['၁','၂','၃','၄','၅','၆','၇','၈','၉','၀'],
                                 ['1','2','3','4','5','6','7','8','9','0'],
@@ -696,6 +798,7 @@ class TemplateRender{
                 }
             }
         }else{
+            Log::debug('book title not found');
             $props['found'] = false;
         }
 
@@ -798,8 +901,10 @@ class TemplateRender{
     }
     private  function render_sent(){
 
-        $sid = $this->get_param($this->param,"sid",1);
+        $sid = $this->get_param($this->param,"id",1);
         $channel = $this->get_param($this->param,"channel",2);
+        $text = $this->get_param($this->param,"text",2,'both');
+
         if(!empty($channel)){
             $channels = explode(',',$channel);
         }else{
@@ -823,6 +928,14 @@ class TemplateRender{
             $tpl = "sentedit";
         }
 
+        //输出引用
+        $arrSid = explode('-',$sid);
+        $bookPara = array_slice($arrSid,0,2);
+        if(!isset($GLOBALS['ref_sent'])){
+            $GLOBALS['ref_sent'] = array();
+        }
+        $GLOBALS['ref_sent'][] = $bookPara;
+
         switch ($this->format) {
             case 'react':
                 $output = [
@@ -840,6 +953,11 @@ class TemplateRender{
                 break;
             case 'text':
                 $output = '';
+                if(isset($props['origin']) && is_array($props['origin'])){
+                    foreach ($props['origin'] as $key => $value) {
+                        $output .= $value['html'];
+                    }
+                }
                 if(isset($props['translation']) && is_array($props['translation'])){
                     foreach ($props['translation'] as $key => $value) {
                         $output .= $value['html'];
@@ -848,11 +966,23 @@ class TemplateRender{
                 break;
             case 'html':
                 $output = '';
-                if(isset($props['translation']) && is_array($props['translation'])){
-                    foreach ($props['translation'] as $key => $value) {
-                        $output .= '<span class="sentence">'.$value['html'].'</span>';
+                $output .= '<span class="sentence">';
+                if($text === 'both' || $text === 'origin'){
+                    if(isset($props['origin']) && is_array($props['origin'])){
+                        foreach ($props['origin'] as $key => $value) {
+                            $output .= '<span class="origin">'.$value['html'].'</span>';
+                        }
                     }
                 }
+                if($text === 'both' || $text === 'translation'){
+                    if(isset($props['translation']) && is_array($props['translation'])){
+                        foreach ($props['translation'] as $key => $value) {
+                            $output .= '<span class="translation">'.$value['html'].'</span>';
+                        }
+                    }
+                }
+
+                $output .= '</span>';
                 break;
             case 'tex':
                 $output = '';
@@ -864,23 +994,59 @@ class TemplateRender{
                 break;
             case 'simple':
                 $output = '';
-                if(isset($props['translation']) &&
-                   is_array($props['translation']) &&
-                   count($props['translation']) > 0
-                   ){
-                    $sentences = $props['translation'];
-                    foreach ($sentences as $key => $value) {
-                        $output .= $value['html'];
+                if($text === 'both' || $text === 'origin'){
+                    if(empty($output)){
+                        if(isset($props['origin']) &&
+                                is_array($props['origin']) &&
+                                count($props['origin']) > 0
+                                ){
+                            foreach ($props['origin'] as $key => $value) {
+                                $output .= trim($value['html']);
+                            }
+                        }
                     }
                 }
-                if(empty($output)){
-                    if(isset($props['origin']) &&
-                            is_array($props['origin']) &&
-                            count($props['origin']) > 0
-                            ){
-                        $sentences = $props['origin'];
-                        foreach ($sentences as $key => $value) {
-                            $output .= $value['html'];
+                if($text === 'both' || $text === 'translation'){
+                    if(isset($props['translation']) &&
+                    is_array($props['translation']) &&
+                    count($props['translation']) > 0
+                    ){
+                        foreach ($props['translation'] as $key => $value) {
+                            $output .= trim($value['html']);
+                        }
+                    }
+                }
+                break;
+            case 'markdown':
+                $output = '';
+                if($text === 'both' || $text === 'origin'){
+                    if($this->options['origin'] === true ||
+                       $this->options['origin'] === 'true'){
+                        if(isset($props['origin']) && is_array($props['origin'])){
+                            foreach ($props['origin'] as $key => $value) {
+                                $output .= trim($value['html']);
+                            }
+                        }
+                    }
+                }
+                if($text === 'both' || $text === 'translation'){
+                    if($this->options['translation']  === true ||
+                       $this->options['translation']  === 'true'){
+                        if(isset($props['translation']) &&
+                            is_array($props['translation']) &&
+                            count($props['translation'])>0){
+                            foreach ($props['translation'] as $key => $value) {
+                                $output .= trim($value['html']);
+                            }
+                        }else{
+                            if($text === 'translation'){
+                                //无译文用原文代替
+                                if(isset($props['origin']) && is_array($props['origin'])){
+                                    foreach ($props['origin'] as $key => $value) {
+                                        $output .= trim($value['html']);
+                                    }
+                                }
+                            }
                         }
                     }
                 }
@@ -1037,6 +1203,81 @@ class TemplateRender{
         return $output;
     }
 
+    //论文后面的参考资料
+    private  function render_ref(){
+        $references = array();
+        $counter = 0;
+
+        if(isset($GLOBALS['ref_sent'])){
+            $hasBooks = array();
+            $book_titles = BookSeries::select(['book','paragraph','title','sn'])
+                                ->orderBy('sn','DESC')->get();
+            $bTitles = array();
+            foreach ($book_titles as $key => $book) {
+                $bTitles[] = [
+                    'book'=>$book->book,
+                    'paragraph'=>$book->paragraph,
+                    'title'=>$book->title
+                ];
+            }
+            foreach ($GLOBALS['ref_sent'] as $key => $ref) {
+                $books = array_filter($bTitles,function($value) use($ref){
+                    return $value['book'] === (int)$ref[0];
+                });
+                if(count($books)>0){
+                    foreach ($books as $key => $book) {
+                        if($book['paragraph'] < (int)$ref[1]){
+                            if(!isset($hasBooks[$book['title']])){
+                                $hasBooks[$book['title']] = 1;
+                                $counter++;
+                                $references[] = [
+                                    'sn'=>$counter,
+                                    'title'=>$book['title'],
+                                    'copyright'=>'CSCD V4 VRI 2008'
+                                ];
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        $props = [
+                    "pali" => $references,
+                ];
+
+        switch ($this->format) {
+            case 'react':
+                $output = [
+                    'props'=>base64_encode(\json_encode($props)),
+                    'html'=>'',
+                    'tag'=>'div',
+                    'tpl'=>'reference',
+                    ];
+                break;
+            case 'unity':
+                $output = [
+                    'props'=>base64_encode(\json_encode($props)),
+                    'tpl'=>'reference',
+                    ];
+                break;
+            case 'markdown':
+                $output = '';
+                foreach ($references as $key => $reference) {
+                    $output .= '[' . $reference['sn'] . '] **' . ucfirst($reference['title']) . '** ';
+                    $output .= $reference['copyright']. "\n\n";
+                }
+                break;
+            default:
+                $output = '';
+                foreach ($references as $key => $reference) {
+                    $output .= '[' . $reference['sn'] . '] ' . ucfirst($reference['title']) . ' ';
+                    $output .= $reference['copyright']. "\n";
+                }
+                break;
+        }
+        return $output;
+    }
+
     private  function get_param(array $param,string $name,int $id,string $default=''){
         if(isset($param[$name])){
             return trim($param[$name]);
@@ -1046,4 +1287,19 @@ class TemplateRender{
             return $default;
         }
     }
+
+    private function mb_trim($str,string $character_mask = ' ', $charset = "UTF-8") {
+        $start = 0;
+        $end = mb_strlen($str, $charset) - 1;
+        $chars = preg_split('//u', $character_mask, -1, PREG_SPLIT_NO_EMPTY);
+        while ($start <= $end && in_array(mb_substr($str, $start, 1, $charset),$chars)) {
+            $start++;
+        }
+
+        while ($end >= $start && in_array(mb_substr($str, $end, 1, $charset),$chars) ) {
+            $end--;
+        }
+
+        return mb_substr($str, $start, $end - $start + 1, $charset);
+    }
 }

+ 2 - 0
app/Http/Api/quote-my.csv

@@ -118,6 +118,8 @@ visuddhi,1,ဝိသုဒ္ဓိ၊၁,visuddhimagga,1
 visuddhi,2,ဝိသုဒ္ဓိ၊၂,visuddhimagga,2
 visuddhi,ṭī,1,ဝိသုဒ္ဓိ၊ဋီ၊၁,visuddhimagga-mahāṭīkā,1
 visuddhi,ṭī,2,ဝိသုဒ္ဓိ၊ဋီ၊၂,visuddhimagga-mahāṭīkā,2
+visuddhi,ṭī,1,မဟာဋီ၊၁,visuddhimagga-mahāṭīkā,1
+visuddhi,ṭī,2,မဟာဋီ၊၂,visuddhimagga-mahāṭīkā,2
 yamaka,1,ယမက၊၁,yamakapāḷi,1
 yamaka,2,ယမက၊၂,yamakapāḷi,2
 yamaka,3,ယမက၊၃,yamakapāḷi,3

+ 101 - 0
app/Http/Controllers/AiTranslateController.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use App\Models\PaliText;
+
+class AiTranslateController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        return $this->fetch(strip_tags($request->get('origin')));
+    }
+
+    private function fetch($origin){
+        $url = 'https://api.moonshot.cn/v1/chat/completions';
+        $param = [
+                "model" => "moonshot-v1-8k",
+                "messages" => [
+                    ["role" => "system","content" => "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"],
+                    ["role" => "user","content" => $origin."\n请翻译上述巴利文。"],
+                ],
+                "temperature" => 0.3,
+        ];
+        $response = Http::withToken('sk-kwjHIMh3PoWwUwQyKdT3KHvNe8Es19SUiujGrxtH09uDQCui')
+                        ->post($url,$param);
+        if($response->failed()){
+            $this->error('http request error'.$response->json('message'));
+            Log::error('http request error', ['data'=>$response->json()]);
+            return $this->error($response->json(),200,200);
+        }else{
+            return $this->ok($response->json());
+        }
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+        $para = explode('-',$id);
+        if(count($para) >= 2){
+            $content = PaliText::where('book',$para[0])
+                        ->where('paragraph',$para[1])
+                        ->value('text');
+            if(!empty($content)){
+                return $this->fetch($content);
+            }else{
+                return $this->error('no content',200,200);
+            }
+        }else{
+            return $this->error('参数错误',403,403);
+        }
+    }
+
+    /**
+     * 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)
+    {
+        //
+    }
+}

+ 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)
+    {
+        //
+    }
+}

+ 14 - 4
app/Http/Controllers/ArticleMapController.php

@@ -23,10 +23,12 @@ class ArticleMapController extends Controller
         //
         switch ($request->get('view')) {
             case 'anthology':
-                $table = ArticleCollection::where('collect_id',$request->get('id'));
+                $table = ArticleCollection::where('collect_id',$request->get('id'))
+                            ->leftJoin('articles','articles.uid','=','article_collections.article_id');
                 break;
             case 'article':
-                $table = ArticleCollection::where('article_id',$request->get('id'));
+                $table = ArticleCollection::where('article_id',$request->get('id'))
+                            ->leftJoin('articles','articles.uid','=','article_collections.article_id');
                 break;
         }
         $count = $table->count();
@@ -54,8 +56,16 @@ class ArticleMapController extends Controller
             if($request->has('lazy') && $count > 300){
                 $table = $table->where('level',1);
             }
-            $result = $table->select(['id','collect_id','article_id','level','title','children','editor_id','deleted_at'])
-                        ->orderBy('id')->get();
+            $result = $table->select([
+                'article_collections.id',
+                'collect_id','article_id',
+                'level',
+                'article_collections.title',
+                'children',
+                'article_collections.editor_id',
+                'article_collections.deleted_at',
+                'articles.status'
+                ])->orderBy('id')->get();
         }
 
         return $this->ok(["rows"=>ArticleMapResource::collection($result),"count"=>$count]);

+ 1 - 0
app/Http/Controllers/CorpusController.php

@@ -615,6 +615,7 @@ class CorpusController extends Controller
                             'para'=>$arrSentId[1],
                             'channels'=>$channelsId,
                             'sentences'=>$sentInPara,
+                            'mode'=>$mode,
                         ])) ;
                     $content[] = "<MdTpl tpl='para-shell' props='{$markProps}' >";
                 }

+ 75 - 0
app/Http/Controllers/EditableSentenceController.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Sentence;
+use Illuminate\Http\Request;
+
+class EditableSentenceController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * 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  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function show(string $sentenceId)
+    {
+        //
+        $sentence = Sentence::find($sentenceId);
+        $sentId = $sentence->book_id . '-'.
+                    $sentence->paragraph .'-'.
+                    $sentence->word_start .'-'.
+                    $sentence->word_end;
+        $corpus = new CorpusController;
+        $props = $corpus->getSentTpl($sentId,[$sentence->channel_uid],
+                    'edit',true,
+                    'react');
+        return $this->ok($props);
+
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Sentence $sentence)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Sentence $sentence)
+    {
+        //
+    }
+}

+ 13 - 6
app/Http/Controllers/NissayaCardController.php

@@ -70,8 +70,6 @@ class NissayaCardController extends Controller
                     ]);
                     $cardData['ending']['html']  = $mdRender->convert($endingTerm->note,[],null);
             }
-
-
         }
 
         $myEnding = NissayaEnding::where('ending',$nissayaEnding)
@@ -107,6 +105,7 @@ class NissayaCardController extends Controller
                     $localCase  =[];
                     foreach ($cases as $case) {
                         $localCase[] = ['label'=>__("grammar.".$case),
+                                        'case'=>$case,
                                         'link'=>config('mint.server.dashboard_base_path').'/term/list/'.$case
                                         ];
                     }
@@ -121,9 +120,11 @@ class NissayaCardController extends Controller
                 if(isset($linkTos->case) && is_array($linkTos->case) && count($linkTos->case)>0){
                     $localTo  =[];
                     foreach ($linkTos->case as $to) {
-                        $localTo[] = ['label'=>__("grammar.".$to),
-                                        'link'=>config('mint.server.dashboard_base_path').'/term/list/'.$to
-                                        ];
+                        $localTo[] = [
+                            'label'=>__("grammar.".$to),
+                            'case'=>$to,
+                            'link'=>config('mint.server.dashboard_base_path').'/term/list/'.$to
+                            ];
                     }
 
                     # 格位
@@ -140,7 +141,13 @@ class NissayaCardController extends Controller
                                                 ->first();
 
                     if($localCategory){
-                        $newLine['category']['note'] = $localCategory->note;
+                        $mdRender = new MdRender(
+                            [
+                                'mode'=>'read',
+                                'format'=>'text',
+                                'lang'=>$endingTerm->lang,
+                            ]);
+                        $newLine['category']['note'] = $mdRender->convert($localCategory->note,[],null);
                         $newLine['category']['meaning'] =$localCategory->meaning;
                     }else{
                         $newLine['category']['note'] = $relation['category'];

+ 66 - 0
app/Http/Controllers/NissayaCoverController.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Tools\RedisClusters;
+
+class NissayaCoverController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+        $cover = RedisClusters::get('/statistics/nissaya/cover');
+        return $this->ok($cover);
+    }
+
+    /**
+     * 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)
+    {
+        //
+    }
+}

+ 1 - 1
app/Http/Controllers/TermVocabularyController.php

@@ -17,7 +17,7 @@ class TermVocabularyController extends Controller
     public function index(Request $request)
     {
         //
-        $table = DhammaTerm::select(['guid','word','tag','meaning']);
+        $table = DhammaTerm::select(['guid','word','tag','meaning','other_meaning']);
         switch ($request->get('view')) {
             case "grammar":
                 $localTerm = ChannelApi::getSysChannel(

+ 21 - 1
app/Http/Controllers/WbwSentenceController.php

@@ -13,6 +13,7 @@ use App\Http\Api\AuthApi;
 use App\Http\Api\ShareApi;
 use App\Http\Api\ChannelApi;
 use App\Http\Api\CourseApi;
+use App\Models\Sentence;
 
 class WbwSentenceController extends Controller
 {
@@ -168,8 +169,27 @@ class WbwSentenceController extends Controller
      * @param  \App\Models\Wbw  $wbw
      * @return \Illuminate\Http\Response
      */
-    public function destroy(Wbw $wbw)
+    public function destroy(Request $request,string $sentId)
     {
         //
+        //鉴权
+        $user = AuthApi::current($request);
+        if(!$user ){
+            //未登录用户
+            return $this->error(__('auth.failed'),401,401);
+        }
+        $channelId = $request->get('channel');
+        if(!ChannelApi::canManageByUser($channelId,$user['user_uid'])){
+            return $this->error(__('auth.failed'),403,403);
+        }
+        $sent = explode('-',$sentId);
+        $wbwBlockId = WbwBlock::where('book_id',$sent[0])
+                        ->where('paragraph',$sent[1])
+                        ->where('channel_uid',$channelId)
+                        ->value('uid');
+        $delete = Wbw::where('block_uid',$wbwBlockId)
+                    ->whereBetween('wid',[$sent[2],$sent[3]])
+                    ->delete();
+        return $this->ok($delete);
     }
 }

+ 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;

+ 1 - 0
app/Http/Resources/ArticleMapResource.php

@@ -25,6 +25,7 @@ class ArticleMapResource extends JsonResource
             "title" => $this->title,
             "editor"=> UserApi::getById($this->editor_id),
             "children" => $this->children,
+            'status' => $this->status,
             "deleted_at" => $this->deleted_at,
             "created_at" => $this->created_at,
             "updated_at" => $this->updated_at,

+ 11 - 4
app/Http/Resources/ArticleResource.php

@@ -66,9 +66,10 @@ class ArticleResource extends JsonResource
         //渲染简化版标题
         $channels = [];
         if($request->has('channel')){
+            //有channel
             $channels = explode('_',$request->get('channel')) ;
         }else if(isset($anthology) && $anthology && !empty($anthology->default_channel)){
-            //使用文集channel
+            //没有channel,使用文集channel
             $channels[] = $anthology->default_channel;
         }
         $mdRender = new MdRender(['format'=>'simple']);
@@ -194,9 +195,15 @@ class ArticleResource extends JsonResource
 
             $mode = $request->get('mode','read');
             $format = $request->get('format','react');
-            $data["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"] = $htmlRender->convert($this->content,$channels);
             if(empty($this->summary)){
                 $data["_summary"] = MdRender::render($this->content,
                                                     $channels,$query_id,$mode,

+ 43 - 17
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');
@@ -196,7 +222,7 @@ class ExportDownload
         }
         $this->downloadUrl = $s3Link;
         $this->setStatus(1,'export chapter done');
-        Log::debug('export chapter done, upload filename='.$tmpFile);
+        Log::debug('export chapter done, upload',['filename'=>$tmpFile,'url'=>$s3Link] );
         unlink($zipFile);
         return true;
     }

+ 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"),
 

+ 2 - 1
database/export/sentence.sql

@@ -188,7 +188,8 @@ ON "sentence_translation" (
 -- ----------------------------
 CREATE UNIQUE INDEX "name"
 ON "tag" (
-  "name" ASC
+  "name",
+  "owner_id"
 );
 
 -- ----------------------------

+ 34 - 0
database/migrations/2024_07_23_032240_add_type2_in_channels.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddType2InChannels extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+            $table->string('type2')->default('translation');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+            $table->dropColumn('type2');
+        });
+    }
+}

+ 37 - 0
database/migrations/2024_07_23_033220_migrate_type_in_channels.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class MigrateTypeInChannels extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+        });
+
+        DB::transaction(function () {
+            $sql = 'UPDATE channels SET type2 = type;';
+            DB::select($sql);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+        });
+    }
+}

+ 36 - 0
database/migrations/2024_07_23_033823_change_type2_to_type_in_channels.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class ChangeType2ToTypeInChannels extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+            $table->renameColumn('type', 'type_old');
+            $table->renameColumn('type2', 'type');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('channels', function (Blueprint $table) {
+            //
+            $table->renameColumn('type', 'type2');
+            $table->renameColumn('type_old','type');
+        });
+    }
+}

+ 0 - 4
public/app/dbadmin/.gitignore

@@ -1,4 +0,0 @@
-/user/config.php
-/system/config.php
-/3rd/config.php
-/palicanon/config.php

+ 0 - 82
public/app/dbadmin/3rd/config.sample.php

@@ -1,82 +0,0 @@
-<?php 
-//
-// This is sample configuration file
-//
-// You can configure phpliteadmin in one of 2 ways:
-// 1. Rename phpliteadmin.config.sample.php to phpliteadmin.config.php and change parameters in there.
-//    You can set only your custom settings in phpliteadmin.config.php. All other settings will be set to defaults.
-// 2. Change parameters directly in main phpliteadmin.php file
-//
-// Please see https://bitbucket.org/phpliteadmin/public/wiki/Configuration for more details
-
-//password to gain access
-$password = 'dhamma';
-
-//directory relative to this file to search for databases (if false, manually list databases in the $databases variable)
-$directory = '../../../tmp/appdata/dict/3rd';
-
-//whether or not to scan the subdirectories of the above directory infinitely deep
-$subdirectories = false;
-
-//if the above $directory variable is set to false, you must specify the databases manually in an array as the next variable
-//if any of the databases do not exist as they are referenced by their path, they will be created automatically
-$databases = array(
-	array(
-		'path'=> 'database1.sqlite',
-		'name'=> 'Database 1'
-	),
-	array(
-		'path'=> 'database2.sqlite',
-		'name'=> 'Database 2'
-	),
-);
-
-
-/* ---- Interface settings ---- */
-
-// Theme! If you want to change theme, save the CSS file in same folder of phpliteadmin or in folder "themes"
-$theme = 'phpliteadmin.css';
-
-// the default language! If you want to change it, save the language file in same folder of phpliteadmin or in folder "languages"
-// More about localizations (downloads, how to translate etc.): https://bitbucket.org/phpliteadmin/public/wiki/Localization
-$language = 'en';
-
-// set default number of rows. You need to relog after changing the number
-$rowsNum = 30;
-
-// reduce string characters by a number bigger than 10
-$charsNum = 300;
-
-// maximum number of SQL queries to save in the history
-$maxSavedQueries = 10;
-
-/* ---- Custom functions ---- */
-
-//a list of custom functions that can be applied to columns in the databases
-//make sure to define every function below if it is not a core PHP function
-$custom_functions = array(
-	'md5', 'sha1', 'time', 'strtotime',
-	// add the names of your custom functions to this array
-	/* 'leet_text', */
-);
-
-// define your custom functions here
-/*
-function leet_text($value)
-{
-  return strtr($value, 'eaAsSOl', '344zZ01');
-}
-*/
-
-
-/* ---- Advanced options ---- */
-
-//changing the following variable allows multiple phpLiteAdmin installs to work under the same domain.
-$cookie_name = 'pla3412';
-
-//whether or not to put the app in debug mode where errors are outputted
-$debug = false;
-
-// the user is allowed to create databases with only these extensions
-$allowed_extensions = array('db','db3','sqlite','sqlite3');
-

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 6090
public/app/dbadmin/3rd/pla.php


+ 0 - 82
public/app/dbadmin/palicanon/config.sample.php

@@ -1,82 +0,0 @@
-<?php 
-//
-// This is sample configuration file
-//
-// You can configure phpliteadmin in one of 2 ways:
-// 1. Rename phpliteadmin.config.sample.php to phpliteadmin.config.php and change parameters in there.
-//    You can set only your custom settings in phpliteadmin.config.php. All other settings will be set to defaults.
-// 2. Change parameters directly in main phpliteadmin.php file
-//
-// Please see https://bitbucket.org/phpliteadmin/public/wiki/Configuration for more details
-
-//password to gain access
-$password = 'dhamma';
-
-//directory relative to this file to search for databases (if false, manually list databases in the $databases variable)
-$directory = "../../../tmp/appdata/palicanon";
-
-//whether or not to scan the subdirectories of the above directory infinitely deep
-$subdirectories = false;
-
-//if the above $directory variable is set to false, you must specify the databases manually in an array as the next variable
-//if any of the databases do not exist as they are referenced by their path, they will be created automatically
-$databases = array(
-	array(
-		'path'=> 'database1.sqlite',
-		'name'=> 'Database 1'
-	),
-	array(
-		'path'=> 'database2.sqlite',
-		'name'=> 'Database 2'
-	),
-);
-
-
-/* ---- Interface settings ---- */
-
-// Theme! If you want to change theme, save the CSS file in same folder of phpliteadmin or in folder "themes"
-$theme = 'phpliteadmin.css';
-
-// the default language! If you want to change it, save the language file in same folder of phpliteadmin or in folder "languages"
-// More about localizations (downloads, how to translate etc.): https://bitbucket.org/phpliteadmin/public/wiki/Localization
-$language = 'en';
-
-// set default number of rows. You need to relog after changing the number
-$rowsNum = 30;
-
-// reduce string characters by a number bigger than 10
-$charsNum = 300;
-
-// maximum number of SQL queries to save in the history
-$maxSavedQueries = 10;
-
-/* ---- Custom functions ---- */
-
-//a list of custom functions that can be applied to columns in the databases
-//make sure to define every function below if it is not a core PHP function
-$custom_functions = array(
-	'md5', 'sha1', 'time', 'strtotime',
-	// add the names of your custom functions to this array
-	/* 'leet_text', */
-);
-
-// define your custom functions here
-/*
-function leet_text($value)
-{
-  return strtr($value, 'eaAsSOl', '344zZ01');
-}
-*/
-
-
-/* ---- Advanced options ---- */
-
-//changing the following variable allows multiple phpLiteAdmin installs to work under the same domain.
-$cookie_name = 'pla3412';
-
-//whether or not to put the app in debug mode where errors are outputted
-$debug = false;
-
-// the user is allowed to create databases with only these extensions
-$allowed_extensions = array('db','db3','sqlite','sqlite3');
-

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 6090
public/app/dbadmin/palicanon/pla.php


+ 0 - 82
public/app/dbadmin/system/config.sample.php

@@ -1,82 +0,0 @@
-<?php 
-//
-// This is sample configuration file
-//
-// You can configure phpliteadmin in one of 2 ways:
-// 1. Rename phpliteadmin.config.sample.php to phpliteadmin.config.php and change parameters in there.
-//    You can set only your custom settings in phpliteadmin.config.php. All other settings will be set to defaults.
-// 2. Change parameters directly in main phpliteadmin.php file
-//
-// Please see https://bitbucket.org/phpliteadmin/public/wiki/Configuration for more details
-
-//password to gain access
-$password = 'dhamma';
-
-//directory relative to this file to search for databases (if false, manually list databases in the $databases variable)
-$directory = '../../../tmp/appdata/dict/system';
-
-//whether or not to scan the subdirectories of the above directory infinitely deep
-$subdirectories = false;
-
-//if the above $directory variable is set to false, you must specify the databases manually in an array as the next variable
-//if any of the databases do not exist as they are referenced by their path, they will be created automatically
-$databases = array(
-	array(
-		'path'=> 'database1.sqlite',
-		'name'=> 'Database 1'
-	),
-	array(
-		'path'=> 'database2.sqlite',
-		'name'=> 'Database 2'
-	),
-);
-
-
-/* ---- Interface settings ---- */
-
-// Theme! If you want to change theme, save the CSS file in same folder of phpliteadmin or in folder "themes"
-$theme = 'phpliteadmin.css';
-
-// the default language! If you want to change it, save the language file in same folder of phpliteadmin or in folder "languages"
-// More about localizations (downloads, how to translate etc.): https://bitbucket.org/phpliteadmin/public/wiki/Localization
-$language = 'en';
-
-// set default number of rows. You need to relog after changing the number
-$rowsNum = 30;
-
-// reduce string characters by a number bigger than 10
-$charsNum = 300;
-
-// maximum number of SQL queries to save in the history
-$maxSavedQueries = 10;
-
-/* ---- Custom functions ---- */
-
-//a list of custom functions that can be applied to columns in the databases
-//make sure to define every function below if it is not a core PHP function
-$custom_functions = array(
-	'md5', 'sha1', 'time', 'strtotime',
-	// add the names of your custom functions to this array
-	/* 'leet_text', */
-);
-
-// define your custom functions here
-/*
-function leet_text($value)
-{
-  return strtr($value, 'eaAsSOl', '344zZ01');
-}
-*/
-
-
-/* ---- Advanced options ---- */
-
-//changing the following variable allows multiple phpLiteAdmin installs to work under the same domain.
-$cookie_name = 'pla3412';
-
-//whether or not to put the app in debug mode where errors are outputted
-$debug = false;
-
-// the user is allowed to create databases with only these extensions
-$allowed_extensions = array('db','db3','sqlite','sqlite3');
-

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 6090
public/app/dbadmin/system/pla.php


+ 0 - 82
public/app/dbadmin/user/config.sample.php

@@ -1,82 +0,0 @@
-<?php 
-//
-// This is sample configuration file
-//
-// You can configure phpliteadmin in one of 2 ways:
-// 1. Rename phpliteadmin.config.sample.php to phpliteadmin.config.php and change parameters in there.
-//    You can set only your custom settings in phpliteadmin.config.php. All other settings will be set to defaults.
-// 2. Change parameters directly in main phpliteadmin.php file
-//
-// Please see https://bitbucket.org/phpliteadmin/public/wiki/Configuration for more details
-
-//password to gain access
-$password = 'dhamma';
-
-//directory relative to this file to search for databases (if false, manually list databases in the $databases variable)
-$directory = "../../../tmp/user";
-
-//whether or not to scan the subdirectories of the above directory infinitely deep
-$subdirectories = false;
-
-//if the above $directory variable is set to false, you must specify the databases manually in an array as the next variable
-//if any of the databases do not exist as they are referenced by their path, they will be created automatically
-$databases = array(
-	array(
-		'path'=> 'database1.sqlite',
-		'name'=> 'Database 1'
-	),
-	array(
-		'path'=> 'database2.sqlite',
-		'name'=> 'Database 2'
-	),
-);
-
-
-/* ---- Interface settings ---- */
-
-// Theme! If you want to change theme, save the CSS file in same folder of phpliteadmin or in folder "themes"
-$theme = 'phpliteadmin.css';
-
-// the default language! If you want to change it, save the language file in same folder of phpliteadmin or in folder "languages"
-// More about localizations (downloads, how to translate etc.): https://bitbucket.org/phpliteadmin/public/wiki/Localization
-$language = 'en';
-
-// set default number of rows. You need to relog after changing the number
-$rowsNum = 30;
-
-// reduce string characters by a number bigger than 10
-$charsNum = 300;
-
-// maximum number of SQL queries to save in the history
-$maxSavedQueries = 10;
-
-/* ---- Custom functions ---- */
-
-//a list of custom functions that can be applied to columns in the databases
-//make sure to define every function below if it is not a core PHP function
-$custom_functions = array(
-	'md5', 'sha1', 'time', 'strtotime',
-	// add the names of your custom functions to this array
-	/* 'leet_text', */
-);
-
-// define your custom functions here
-/*
-function leet_text($value)
-{
-  return strtr($value, 'eaAsSOl', '344zZ01');
-}
-*/
-
-
-/* ---- Advanced options ---- */
-
-//changing the following variable allows multiple phpLiteAdmin installs to work under the same domain.
-$cookie_name = 'pla3412';
-
-//whether or not to put the app in debug mode where errors are outputted
-$debug = false;
-
-// the user is allowed to create databases with only these extensions
-$allowed_extensions = array('db','db3','sqlite','sqlite3');
-

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 6090
public/app/dbadmin/user/pla.php


+ 0 - 13
public/app/studio/file_save.php

@@ -1,13 +0,0 @@
-<?php 
-
-$filename=$_POST["filename"];
-$data=$_POST["data"];
-
-//save data file
-$myfile = fopen($filename, "w") or die("Unable to open file!");
-fwrite($myfile, $data);
-fclose($myfile);
-
-echo("Successful");
-?>
-

+ 39 - 0
resources/mustache/article/html/main.html

@@ -8,6 +8,9 @@
             text-indent: 2em;
             line-height: 1.7em;
         }
+        .sentence>p{
+            display: inline;
+        }
         a {
             text-decoration: none;
         }
@@ -18,6 +21,9 @@
         .row {
             display: flex;
         }
+        img{
+            max-width: 100%;
+        }
         @media screen and (max-width: 960px){
             .row {
                 flex-direction: column;
@@ -34,6 +40,39 @@
         .origin {
             font-family: times;
         }
+
+        table {
+            border-spacing: 0;
+            border-collapse: collapse;
+            display: block;
+            width: -webkit-max-content;
+            width: max-content;
+            max-width: 100%;
+        }
+
+        td,
+        th {
+            padding: 0;
+        }
+
+        table th {
+            font-weight: 600;
+        }
+
+        table th,
+        table td {
+            padding: 6px 13px;
+            border: 1px solid black;
+        }
+
+        table tr {
+            background-color: #ffffff;
+            border-top: 1px solid hsl(210, 18%, 87%);
+        }
+
+        table img {
+            background-color: transparent;
+        }
     </style>
 </head>
 <body>

+ 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


+ 9 - 0
routes/api.php

@@ -94,6 +94,10 @@ use App\Http\Controllers\ResetPasswordController;
 use App\Http\Controllers\DiscussionCountController;
 use App\Http\Controllers\TagsInChapterCountController;
 use App\Http\Controllers\TagMapController;
+use App\Http\Controllers\EditableSentenceController;
+use App\Http\Controllers\ArticleFtsController;
+use App\Http\Controllers\NissayaCoverController;
+use App\Http\Controllers\AiTranslateController;
 
 /*
 |--------------------------------------------------------------------------
@@ -244,6 +248,11 @@ Route::group(['prefix' => 'v2'],function(){
     Route::apiResource('discussion-count',DiscussionCountController::class);
     Route::apiResource('tags-in-chapter',TagsInChapterCountController::class);
     Route::apiResource('tag-map',TagMapController::class);
+    Route::apiResource('editable-sentence',EditableSentenceController::class);
+    Route::apiResource('article-fts',ArticleFtsController::class);
+    Route::apiResource('nissaya-cover',NissayaCoverController::class);
+    Route::apiResource('ai-translate',AiTranslateController::class);
+
 
     Route::get('download/{type1}/{type2}/{uuid}/{filename}', function ($type1,$type2,$uuid,$filename) {
         header("Content-Type: {$type1}/{$type1}");

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно