From ef87aebed582497804baf597931c693bc174f689 Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 29 Jan 2025 14:51:52 -0800 Subject: [PATCH] add markdown parser to wiki pages Signed-off-by: Aditya --- src/configs/Config.php | 2 +- src/controllers/Controller.php | 11 ++ src/controllers/components/Component.php | 2 + .../components/SocialComponent.php | 17 ++- src/library/VersionFunctions.php | 8 ++ src/library/WikiParser.php | 100 ++++++++++++++++++ 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, 227 insertions(+), 16 deletions(-) diff --git a/src/configs/Config.php b/src/configs/Config.php index 985a40206..f2fe902dd 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', 81); +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 879296c54..8f378db5a 100644 --- a/src/controllers/components/SocialComponent.php +++ b/src/controllers/components/SocialComponent.php @@ -3757,8 +3757,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; @@ -3768,7 +3768,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"] ?? ""; } @@ -4647,6 +4654,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 488b1e7ce..2fbcead9b 100644 --- a/src/library/VersionFunctions.php +++ b/src/library/VersionFunctions.php @@ -2193,3 +2193,11 @@ function upgradeDatabaseVersion81(&$db) $profile_model->updateProfile(C\WORK_DIRECTORY, $new_profile, $profile); } +/** + * 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) +{ + $db->execute("ALTER TABLE GROUP_PAGE ADD MARKDOWN $page_type"); +} diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php index cac4eced0..dd50869d4 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,105 @@ class WikiParser implements CrawlConstants } return $document; } + + public static function parseMarkdown($page_data) { + // Headers + $page_data = preg_replace_callback('/^(#{1,6})\s*(.*)$/m', + function($matches) { + $level = strlen($matches[1]); + return "" . trim($matches[2]) . ""; + }, $page_data); + $page_data = preg_replace('/^(.+)\n=+$/m', '

$1

', $page_data); + $page_data = preg_replace('/^(.+)\n-+$/m', '

$1

', $page_data); + // Bold and Italic + $page_data = preg_replace('/\*\*([^*]+)\*\*/', '$1', + $page_data); + $page_data = preg_replace('/\_\_([^_]+)\_\_/', '$1', + $page_data); + $page_data = preg_replace('/\*([^*]+)\*/', '$1', $page_data); + $page_data = preg_replace('/\_([^_]+)\_/', '$1', $page_data); + // Links and Images + $page_data = preg_replace( + '/!\[([^\]]*)\]\(([^)]+)\)/', '$1', $page_data + ); + $page_data = preg_replace( + '/\[([^\]]+)\]\(([^)]+)\)/', '$1', $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 ""; + }, + $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 "
    \n" . implode("\n", array_map( + function($item) { + return "
  1. " . trim($item) . "
  2. "; + }, + $items + )) . "\n
"; + }, + $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 "
$code
"; + }, $page_data); + // Inline code + $page_data = preg_replace('/`([^`]+)`/', '$1', $page_data); + // Blockquotes + $page_data = preg_replace_callback('/(?:^|\n)>\s*(.+?)(?:\n|$)/s', + function($matches) { + return "
" . trim($matches[1]) . "
"; + }, $page_data); + // Horizontal rules + $page_data = preg_replace( + '/^(?:[\s]*[\*\-_]){3,}[\s]*$/m', + '
', + $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 "

$block

"; + } + 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 25bd221b0..84ddbf44a 100644 --- a/src/locale/en_US/configure.ini +++ b/src/locale/en_US/configure.ini @@ -1237,6 +1237,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 1c7b11015..44b6fa99f 100644 --- a/src/models/GroupModel.php +++ b/src/models/GroupModel.php @@ -1874,9 +1874,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, '
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); @@ -2188,8 +2203,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 4e4c7befe..bea6ccaee 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 ecbd074e0..1629ec1c9 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' >
+ + + id="render-markdown"> +
+
id='page-toc' disabled="disabled" >
+ + + id="render-markdown"> +
+