_binary From a21c6ef938e81c267f15f35d65c2bcbc1764a419 Mon Sep 17 00:00:00 2001
From: Aditya <thesyn3rman@gmail.com>
Date: Tue, 4 Mar 2025 14:17:11 -0800
Subject: [PATCH] markdown parser

Signed-off-by: Aditya <thesyn3rman@gmail.com>
---
 src/configs/Config.php                        |   2 +-
 src/controllers/Controller.php                |  11 ++
 src/controllers/components/Component.php      |   2 +
 .../components/SocialComponent.php            |  17 ++-
 src/library/VersionFunctions.php              |  11 ++
 src/library/WikiParser.php                    | 106 ++++++++++++++++++
 src/locale/en_US/configure.ini                |   1 +
 src/models/GroupModel.php                     |  33 ++++--
 src/models/ProfileModel.php                   |   5 +-
 src/scripts/wiki.js                           |  40 ++++++-
 src/views/elements/WikiElement.php            |  24 ++++
 11 files changed, 236 insertions(+), 16 deletions(-)

diff --git a/src/configs/Config.php b/src/configs/Config.php
index f805ffb2c..1b8451973 100755
--- a/src/configs/Config.php
+++ b/src/configs/Config.php
@@ -162,7 +162,7 @@ nsconddefine('GENERATOR_STRING', "Yioop");
  * Version number for upgrade database function
  * @var int
  */
-nsdefine('DATABASE_VERSION', 82);
+nsdefine('DATABASE_VERSION', 83);
 /**
  * Minimum Version fo Yioop for which keyword ad script
  * still works with this version
diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php
index c5d0f6660..20aad68d1 100755
--- a/src/controllers/Controller.php
+++ b/src/controllers/Controller.php
@@ -35,6 +35,7 @@ use seekquarry\yioop\configs as C;
 use seekquarry\yioop\library as L;
 use seekquarry\yioop\library\AnalyticsManager;
 use seekquarry\yioop\library\UrlParser;
+use seekquarry\yioop\library\WikiParser;
 use seekquarry\yioop\models\Model;
 use seekquarry\yioop\controllers\components\Component;
 use seekquarry\yioop\views\View;
@@ -308,6 +309,16 @@ abstract class Controller
             $data['DISPLAY_MESSAGE'] = $_SESSION['DISPLAY_MESSAGE'];
             unset($_SESSION['DISPLAY_MESSAGE']);
         }
+        if (isset($this->model_instances["group"]) && $view == "group") {
+            $locale_tag = $data['CURRENT_LOCALE_TAG'] ?? C\DEFAULT_LOCALE;
+            $pageInfo = $this->model_instances["group"]->getPageInfoByName(
+                $data["GROUP"]["GROUP_ID"],
+                $data["PAGE_NAME"], $locale_tag, "read");
+            if (is_array($pageInfo)) {
+                $headers = WikiParser::parsePageHeadVars($pageInfo["PAGE"]);
+                $data['render_markdown'] = $headers['render_markdown'];
+            }
+        }
         $this->view($view)->render($data);
     }
     /**
diff --git a/src/controllers/components/Component.php b/src/controllers/components/Component.php
index 63fbe644e..0419663e0 100644
--- a/src/controllers/components/Component.php
+++ b/src/controllers/components/Component.php
@@ -158,6 +158,8 @@ class Component
                 tl('wiki_js_slide_resource_description').'", '.
                 'wiki_element_list_icon :"'. tl('wiki_element_list_icon').'",'.
                 'wiki_element_grid_icon :"'. tl('wiki_element_grid_icon').'",'.
+                'wiki_js_render_markdown:"'.
+                    ($data['render_markdown'] ?? 'false') .'",'.
                 '};';
         }
         if ($id != -1) {
diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php
index c55d06e2f..7b5a82736 100644
--- a/src/controllers/components/SocialComponent.php
+++ b/src/controllers/components/SocialComponent.php
@@ -3825,8 +3825,8 @@ class SocialComponent extends Component implements CrawlConstants
             $page_name = tl('social_component_main');
         }
         $data["GROUP"] = $group;
-        if (in_array($data["MODE"], ["api", "read", "edit", "media",
-            "source"])) {
+        if (in_array($data["MODE"], ["api", "read", "edit",
+                     "media", "source"])) {
             // history action might set page, otherwise...
             if (empty($data["PAGE"]) && empty($data['RESOURCE_NAME'])) {
                 $data["PAGE_NAME"] = $page_name;
@@ -3836,7 +3836,14 @@ class SocialComponent extends Component implements CrawlConstants
                     $page_info = $group_model->getPageInfoByName($group_id,
                         $page_name, $data['CURRENT_LOCALE_TAG'], $data["MODE"]);
                 }
-                $data["PAGE"] = $page_info["PAGE"] ?? "";
+                $page_headers= WikiParser::parsePageHeadVars(
+                    $page_info["PAGE"]);
+                if ($data["MODE"] == "read" &&
+                    $page_headers["render_markdown"] == "true") {
+                    $data["PAGE"] = $page_info["MARKDOWN"] ?? "";
+                } else {
+                    $data["PAGE"] = $page_info["PAGE"] ?? "";
+                }
                 $data["PAGE_ID"] = $page_info["ID"] ?? "";
                 $data["DISCUSS_THREAD"] = $page_info["DISCUSS_THREAD"] ?? "";
             }
@@ -4721,6 +4728,10 @@ EOD;
                         $head_vars[$key] =  trim(
                             $parent->clean($_REQUEST[$key], "string"));
                         switch ($key) {
+                            case 'render_markdown':
+                                $head_vars[$key] = (isset($_REQUEST[$key]) &&
+                                $_REQUEST[$key] === 'true') ? "true" : "false";
+                                break;
                             case 'page_type':
                                 if (!in_array($head_vars[$key], $page_types) &&
                                     ($head_vars[$key][0] != 't' ||
diff --git a/src/library/VersionFunctions.php b/src/library/VersionFunctions.php
index ac513b3f0..56795c9c9 100644
--- a/src/library/VersionFunctions.php
+++ b/src/library/VersionFunctions.php
@@ -2213,3 +2213,14 @@ function upgradeDatabaseVersion82(&$db)
         ON DELETE CASCADE ON UPDATE CASCADE
     )");
 }
+/**
+ * Upgrades a Version 82 version of the Yioop database to a Version 83 version
+ * @param object $db datasource to use to upgrade
+ */
+function upgradeDatabaseVersion83(&$db)
+{
+    $dbinfo = ["DBMS" => C\DBMS, "DB_HOST" => C\DB_HOST,
+    "DB_NAME" => C\DB_NAME, "DB_PASSWORD" => C\DB_PASSWORD];
+    $page_type = $db->textType($dbinfo);
+    $db->execute("ALTER TABLE GROUP_PAGE ADD MARKDOWN $page_type");
+}
diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php
index cac4eced0..a15d403a9 100644
--- a/src/library/WikiParser.php
+++ b/src/library/WikiParser.php
@@ -73,6 +73,7 @@ class WikiParser implements CrawlConstants
         'share_expires' => C\FOREVER,
         'title' => '',
         'toc' => true,
+        'render_markdown' => false,
         'url_shortener' => '',
         'update_description' => false
     ];
@@ -579,6 +580,111 @@ class WikiParser implements CrawlConstants
         }
         return $document;
     }
+    /**
+     * Parse markdown text to html
+     *
+     * @param string $page_data markdown text to parse
+     * @return string equivalent parsed html text
+     */
+    public static function parseMarkdown($page_data)
+    {
+        // Headers
+        $page_data = preg_replace_callback('/^(#{1,6})\s*(.*)$/m',
+            function($matches) {
+                $level = strlen($matches[1]);
+                return "<h$level>" . trim($matches[2]) . "</h$level>";
+        }, $page_data);
+        $page_data = preg_replace('/^(.+)\n=+$/m', '<h1>$1</h1>', $page_data);
+        $page_data = preg_replace('/^(.+)\n-+$/m', '<h2>$1</h2>', $page_data);
+        // Bold and Italic
+        $page_data = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>',
+                                  $page_data);
+        $page_data = preg_replace('/\_\_([^_]+)\_\_/', '<strong>$1</strong>',
+                                   $page_data);
+        $page_data = preg_replace('/\*([^*]+)\*/', '<em>$1</em>', $page_data);
+        $page_data = preg_replace('/\_([^_]+)\_/', '<em>$1</em>', $page_data);
+        // Links and Images
+        $page_data = preg_replace(
+            '/!\[([^\]]*)\]\(([^)]+)\)/', '<img src="$2" alt="$1">', $page_data
+        );
+        $page_data = preg_replace(
+            '/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $page_data
+        );
+        // Lists
+        $page_data = preg_replace_callback(
+            '/(?:^|\n)([*+-]\s+(?:.+?)(?:\n|$))+/s',
+            function($matches) {
+                $items = preg_split(
+                    '/^[*+-]\s+/m',
+                    $matches[0],
+                    -1,
+                    PREG_SPLIT_NO_EMPTY
+                );
+                return "<ul>\n" . implode("\n", array_map(
+                    function($item) {
+                        return "  <li>" . trim($item) . "</li>";
+                    },
+                    $items
+                )) . "\n</ul>";
+            },
+            $page_data
+        );
+        $page_data = preg_replace_callback(
+            '/(?:^|\n)(\d+\.\s+(?:.+?)(?:\n|$))+/s',
+            function($matches) {
+                $items = preg_split(
+                    '/^\d+\.\s+/m',
+                    $matches[0],
+                    -1,
+                    PREG_SPLIT_NO_EMPTY
+                );
+                return "<ol>\n" . implode("\n", array_map(
+                    function($item) {
+                        return "  <li>" . trim($item) . "</li>";
+                    },
+                    $items
+                )) . "\n</ol>";
+            },
+            $page_data
+        );
+        // Code blocks
+        $page_data = preg_replace_callback('/```(\w+)?\n(.*?)\n```/s',
+            function($matches) {
+                $code = htmlspecialchars($matches[2]);
+                $lang = !empty($matches[1])
+                        ? " class=\"language-{$matches[1]}\""
+                        : '';
+                return "<pre><code$lang>$code</code></pre>";
+            }, $page_data);
+        // Inline code
+        $page_data = preg_replace('/`([^`]+)`/', '<code>$1</code>', $page_data);
+        // Blockquotes
+        $page_data = preg_replace_callback('/(?:^|\n)>\s*(.+?)(?:\n|$)/s',
+            function($matches) {
+                return "<blockquote>" . trim($matches[1]) . "</blockquote>";
+            }, $page_data);
+        // Horizontal rules
+        $page_data = preg_replace(
+            '/^(?:[\s]*[\*\-_]){3,}[\s]*$/m',
+            '<hr>',
+            $page_data
+        );
+        // Paragraphs
+        $blocks = preg_split('/\n\s*\n/', $page_data);
+        $blocks = array_map(function($block) {
+            $block = trim($block);
+            if (!preg_match(
+        '/^<\/?(?:p|div|h[1-6]|ul|ol|li|blockquote|pre|hr|table|tr|td|th)>/i',
+                $block
+            )) {
+                return "<p>$block</p>";
+            }
+            return $block;
+        }, $blocks);
+        $page_data = implode("\n\n", $blocks);
+        return $page_data;
+     }
+
     /**
      * Applies all the wiki substitutions of this WikiParser to the document
      * to create an html document
diff --git a/src/locale/en_US/configure.ini b/src/locale/en_US/configure.ini
index 6db5ac9fa..a62c771b5 100644
--- a/src/locale/en_US/configure.ini
+++ b/src/locale/en_US/configure.ini
@@ -1238,6 +1238,7 @@ wiki_element_share_expires = "Expires:"
 wiki_element_page_border = "Page Border:"
 wiki_element_page_theme = "Page Theme:"
 wiki_element_table_of_contents = "Table of Contents:"
+wiki_element_render_as_markdown = "Render as Markdown:"
 wiki_element_title = "Title:"
 wiki_element_meta_author = "Author:"
 wiki_element_meta_robots = "Meta Robots:"
diff --git a/src/models/GroupModel.php b/src/models/GroupModel.php
index 8d7d0f419..bcb495e26 100644
--- a/src/models/GroupModel.php
+++ b/src/models/GroupModel.php
@@ -2060,9 +2060,17 @@ class GroupModel extends Model implements MediaConstants
                 $is_form = true;
             }
             if (!str_contains($page, $end_head)) {
-                $page = WikiParser::makeWikiPageHead() . $end_head . $page;
+                $page = WikiParser::makeWikiPageHead()
+                        . WikiParser::END_HEAD_VARS
+                        . $page;
             }
             $parsed_page = $parser->parse($page);
+            list($page_settings, $page_contents) =
+                WikiParser::parsePageHeadVars($page, true);
+            $markdown = WikiParser::parseMarkdown($page_contents);
+            $markdown = WikiParser::makeWikiPageHead($page_settings)
+                        . $end_head
+                        . $markdown;
             if ($is_form && strstr($page, '<form ') !== false) {
                 return null;
             }
@@ -2083,16 +2091,23 @@ class GroupModel extends Model implements MediaConstants
                     "[{form-hash". L\crawlHash($page_body) . "}]",
                     $parsed_page);
             }
-            $sql = "UPDATE GROUP_PAGE SET PAGE=?, LAST_MODIFIED=? WHERE ID = ?";
-            $result = $db->execute($sql, [$parsed_page, $pubdate, $page_id]);
+            $sql = "UPDATE GROUP_PAGE
+                    SET PAGE=?, MARKDOWN=?, LAST_MODIFIED=?
+                    WHERE ID = ?";
+            $result = $db->execute(
+                $sql,
+                [$parsed_page, $markdown, $pubdate, $page_id]
+            );
         } else {
             $discuss_thread = $this->addGroupItem(0, $group_id, $user_id,
                 $thread_title, $thread_description . " " . date("r", $pubdate),
                 C\WIKI_GROUP_ITEM);
-            $sql = "INSERT INTO GROUP_PAGE (DISCUSS_THREAD, GROUP_ID, TITLE,
-                PAGE, LOCALE_TAG, LAST_MODIFIED) VALUES (?, ?, ?, ?, ?, ?)";
+                $sql = "INSERT INTO GROUP_PAGE (
+                    DISCUSS_THREAD, GROUP_ID, TITLE, PAGE, MARKDOWN,
+                    LOCALE_TAG, LAST_MODIFIED
+                ) VALUES (?, ?, ?, ?, ?, ?, ?)";
             $result = $db->execute($sql, [$discuss_thread, $group_id,
-                $page_name, $parsed_page, $locale_tag, $pubdate]);
+                $page_name, $parsed_page, $markdown, $locale_tag, $pubdate]);
             $page_id = $db->insertID("GROUP_PAGE");
             ImpressionModel::initWithDb($user_id, $page_id, C\WIKI_IMPRESSION,
                 $db);
@@ -2374,8 +2389,10 @@ class GroupModel extends Model implements MediaConstants
                 AND GP.TITLE = ? AND GP.LOCALE_TAG = ? AND HP.PAGE_ID = GP.ID
                 ORDER BY HP.PUBDATE DESC " . $db->limitOffset(0, 1);
         } else {
-            $sql = "SELECT ID, PAGE, DISCUSS_THREAD, LAST_MODIFIED FROM
-                GROUP_PAGE WHERE GROUP_ID = ? AND TITLE=? AND LOCALE_TAG = ?";
+            $sql = "SELECT ID, PAGE, MARKDOWN, DISCUSS_THREAD, LAST_MODIFIED
+                    FROM
+                    GROUP_PAGE
+                    WHERE GROUP_ID = ? AND TITLE = ? AND LOCALE_TAG = ?";
         }
         $result = $db->execute($sql, [$group_id, $name, $locale_tag]);
         if (!$result) {
diff --git a/src/models/ProfileModel.php b/src/models/ProfileModel.php
index 952724baf..2a94141b1 100755
--- a/src/models/ProfileModel.php
+++ b/src/models/ProfileModel.php
@@ -223,8 +223,9 @@ class ProfileModel extends Model
             "GROUP_PAGE" => "CREATE TABLE GROUP_PAGE (
                 ID $serial PRIMARY KEY $auto_increment, GROUP_ID $integer,
                 DISCUSS_THREAD $integer, TITLE VARCHAR(" . C\TITLE_LEN . "),
-                PAGE $page_type, LOCALE_TAG VARCHAR(" . C\NAME_LEN . "),
-                LAST_MODIFIED NUMERIC(" . C\TIMESTAMP_LEN . "))",
+                PAGE $page_type, MARKDOWN $page_type, LOCALE_TAG VARCHAR("
+                . C\NAME_LEN . "), LAST_MODIFIED NUMERIC("
+                . C\TIMESTAMP_LEN . "))",
             "GP_ID_INDEX" => "CREATE INDEX GP_ID_INDEX ON GROUP_PAGE
                  (GROUP_ID, TITLE, LOCALE_TAG)",
             "GP_GROUP_ID_INDEX" => "CREATE INDEX GP_GROUP_ID_INDEX ON GROUP_PAGE
diff --git a/src/scripts/wiki.js b/src/scripts/wiki.js
index 3e5ccb11e..3c1449fcb 100755
--- a/src/scripts/wiki.js
+++ b/src/scripts/wiki.js
@@ -61,6 +61,13 @@ var editor_all_buttons = [];
  * @var Array
  */
 var editor_buttons = [];
+/*
+ * Flag to determine if markdown rendering is enabled
+ * When true, editor buttons will insert markdown syntax
+ * When false, editor buttons will insert wiki syntax
+ * @var Boolean
+ */
+var render_markdown = false;
 /*
  * Object that buffers selection information.
  * @var Object
@@ -226,11 +233,33 @@ function editorize(id)
 }
 /*
  * Method to return standard buttons as an object.
+ * Returns different button configurations based on render_markdown setting.
  *
- * @return Object
+ * @return Object containing button configurations with their markup syntax
  */
 function getStandardButtonsObject()
 {
+    render_markdown = tl?.wiki_js_render_markdown === "true";
+    if (render_markdown) {
+        return {
+            'wikibtn-bold': ['**', '**'],
+            'wikibtn-italic': ['*', '*'],
+            'wikibtn-strike': ['~~', '~~'],
+            'wikibtn-nowiki': ['```', '```'],
+            'wikibtn-hyperlink': ['[', '](url)'],
+            'wikibtn-bullets': ['* ' + tl['wiki_js_bullet'] + ' \n'],
+            'wikibtn-numbers': [tl['wiki_js_enum'] + '. \n'],
+            'wikibtn-hr': ['---\n'],
+            'wikibtn-heading': '', // Handled separately
+            'wikibtn-table': '',
+            'wikibtn-slide': ['=' + tl['wiki_js_slide_sample_title'] + '=\n'
+                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+                + '....' + '\n'],
+            'wikibtn-definitionlist': ['**Term**: Definition\n'],
+        };
+    }
     return {
         'wikibtn-bold': ['---', '---'],
         'wikibtn-italic': ['--', '--'],
@@ -689,7 +718,14 @@ function addWikiHeading(id)
 {
     select_object = elt("wiki-heading-" + id);
     var heading_size = select_object.value;
-    var markup_text = fillChars("=", heading_size);
+    var markup_text;
+    if (render_markdown) {
+        markup_text = fillChars("#", heading_size) + " ";
+        wikify(markup_text, "\n", "wikibtn-heading" + heading_size, id);
+        select_object.selectedIndex = 0;
+        return;
+    }
+    markup_text = fillChars("=", heading_size);
     select_object.selectedIndex = 0;
     wikify(markup_text, markup_text, "wikibtn-heading" + heading_size , id);
 }
diff --git a/src/views/elements/WikiElement.php b/src/views/elements/WikiElement.php
index ac69da8c4..199c0b067 100644
--- a/src/views/elements/WikiElement.php
+++ b/src/views/elements/WikiElement.php
@@ -650,6 +650,18 @@ class WikiElement extends Element implements CrawlConstants
                 ?> id='page-toc' >
             </div>
             <div class="top-margin">
+            <label for="render-markdown"><b>
+            <?= tl('wiki_element_render_as_markdown') ?></b></label>
+            <input type="hidden" name="render_markdown" value="false">
+            <input type="checkbox" name="render_markdown" value="true"
+                <?php
+                    $checked = (isset($data['render_markdown']) &&
+                    $data['render_markdown'] === 'true')
+                    ? 'checked="checked"' : '';
+                    echo $checked;
+                ?> id="render-markdown">
+            </div>
+            <div class="top-margin">
             <label for="page-title"><b><?=tl('wiki_element_title')
             ?></b></label><input type="text" id='page-title'
                 name="title" value="<?=$data['title'] ?>"
@@ -2158,6 +2170,18 @@ class WikiElement extends Element implements CrawlConstants
                 ?> id='page-toc' disabled="disabled" >
             </div>
             <div class="top-margin">
+            <label for="render-markdown"><b>
+            <?= tl('wiki_element_render_as_markdown') ?></b></label>
+            <input type="hidden" name="render_markdown" value="false">
+            <input type="checkbox" name="render_markdown" value="true"
+                <?php
+                    $checked = (isset($data['render_markdown'])
+                    && $data['render_markdown'] === 'true')
+                    ? 'checked="checked"' : '';
+                    echo $checked;
+                ?> id="render-markdown">
+            </div>
+            <div class="top-margin">
             <label for="page-title"><b><?=tl('wiki_element_title')
             ?></b></label><input type="text" id='page-title'
                 disabled="disabled"
-- 
2.39.5 (Apple Git-154)

