diff --git a/Generated/InstallerSetup.exe b/Generated/InstallerSetup.exe index bfe9a2d..24c94ef 100644 Binary files a/Generated/InstallerSetup.exe and b/Generated/InstallerSetup.exe differ diff --git a/appinstaller/appinstaller.rc b/appinstaller/appinstaller.rc index 2157a4a..0d5608a 100644 Binary files a/appinstaller/appinstaller.rc and b/appinstaller/appinstaller.rc differ diff --git a/appinstaller/main.cpp b/appinstaller/main.cpp index 4f0e24e..96bb578 100644 --- a/appinstaller/main.cpp +++ b/appinstaller/main.cpp @@ -1223,7 +1223,7 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form, public IScrip { try { - const auto &pi = *g_pkginfo.begin (); + const auto &pi = g_pkginfo.at (0); const std::wstring &name = pi.identity.name, &publisher = pi.identity.publisher, @@ -1231,7 +1231,9 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form, public IScrip &fullname = pi.identity.package_full_name; std::vector fpkgs; std::wstring err, msg; - HRESULT hr = GetAppxPackages (family, fpkgs, err, msg); + HRESULT hr = S_OK; + if (g_initfile [L"Settings"] [L"AppInstaller:CheckPackageIsIntalled"].read_bool ()) + hr = GetAppxPackages (family, fpkgs, err, msg); bool isfind = false; find_pkginfo findpkg; if (fpkgs.size () > 0) diff --git a/certmgr/certmgr.rc b/certmgr/certmgr.rc index 85706b0..fb2459b 100644 Binary files a/certmgr/certmgr.rc and b/certmgr/certmgr.rc differ diff --git a/notice/notice.rc b/notice/notice.rc index 326cc5f..5b67c05 100644 Binary files a/notice/notice.rc and b/notice/notice.rc differ diff --git a/others/Autosave/autosave_2025-12-07_22-43-55.suf b/others/Autosave/autosave_2025-12-08_23-56-52.suf similarity index 94% rename from others/Autosave/autosave_2025-12-07_22-43-55.suf rename to others/Autosave/autosave_2025-12-08_23-56-52.suf index 7efe1dc..a907fd5 100644 --- a/others/Autosave/autosave_2025-12-07_22-43-55.suf +++ b/others/Autosave/autosave_2025-12-08_23-56-52.suf @@ -687,7 +687,7 @@ %StartProgramsFolderCommon%\\%AppShortcutFolderName% Update -appinstaller update /autoupdate +appinstaller update /checkupdate 0 @@ -953,6 +953,152 @@ 0 0 + +0 +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release\PriFileFormat.dll.config +PriFileFormat.dll.config +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release +config +档案 + +1 +0 +%AppFolder% +1 +0 +0 +1000 +0 +0 +0 +0 +0 +0 +0 +0 + + + + + +0 + +0 +0 +0 +0 + +0 +0 +0 +1 +1 +0 +0 +0 +0 + +32768 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 + + + +All + +None + + +0 +0 +0 + + +0 +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release\priformatcli.dll.metagen +priformatcli.dll.metagen +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release +metagen +档案 + +1 +0 +%AppFolder% +1 +0 +0 +1000 +0 +0 +0 +0 +0 +0 +0 +0 + + + + + +0 + +0 +0 +0 +0 + +0 +0 +0 +1 +1 +0 +0 +0 +0 + +32768 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 + + + +All + +None + + +0 +0 +0 + @@ -2119,7 +2265,7 @@ end 1 1 0 -0 +1 0 1 0 @@ -2180,7 +2326,95 @@ end < &Back &Cancel &Help -Insert your license agreement text here... +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>End User License Agreement (EULA)</title> + <style> + body { + /*font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> + +<body> + <h1>End User License Agreement (EULA)</h1> + + <p>Please read this license agreement carefully before installing or using this software.</p> + + <h2>1. Copyright</h2> + <p>Copyright 漏 2025 Windows Modern. This software and its source code are protected by copyright law.</p> + + <h2>2. License</h2> + <p>This software is licensed under the MIT License. You are free to use, copy, modify, and distribute this software and its source code, including for commercial purposes, subject to the terms of the MIT License.</p> + + <h2>3. Third-Party Components</h2> + <p>This software includes the following third-party open source components, which are subject to their original licenses:</p> + <ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> + </ul> + + <h2>4. Disclaimer</h2> + <p>This software is provided "as is", without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be + liable for any damages arising from the use of this software, whether direct, indirect, incidental, or consequential.</p> + + <h2>5. Acceptance</h2> + <p>By installing or using this software, you agree to this license agreement. If you do not agree, do not install or use the software.</p> + +</body> + +</html> I agree to the terms of this license agreement I do not agree to the terms of this license agreement @@ -2206,7 +2440,92 @@ end < 返回(&B) 取消(&C) 帮助(&H) -在此插入您的许可协议文本... +<!DOCTYPE html> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=GB2312"> + <title>最终用户许可协议 (EULA)</title> + <style> + body { + /*font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> +<body> +<h1>最终用户许可协议 (EULA)</h1> + +<p>请在安装或使用本软件前仔细阅读本许可协议。</p> + +<h2>一、版权声明</h2> +<p>Copyright &#169; 2025 Windows Modern. 本软件及其源码受版权法保护。</p> + +<h2>二、许可范围</h2> +<p>本软件遵循 MIT 许可协议。您可以在符合 MIT 许可条件下自由使用、复制、修改、分发本软件及其源码,包括商业用途。</p> + +<h2>三、第三方组件</h2> +<p>本软件包含以下第三方开源组件,使用这些组件受其原始许可证约束:</p> +<ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> +</ul> + +<h2>四、免责声明</h2> +<p>本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性、特定用途适用性和非侵权的保证。无论在何种情况下,作者或版权持有人均不对因使用本软件产生的任何直接、间接、偶然或特殊损害负责。</p> + +<h2>五、接受条款</h2> +<p>安装或使用本软件即表示您接受本许可协议。如果您不同意本协议,请不要安装或使用本软件。</p> + +</body> +</html> + 我同意该许可协议的条款 我不同意该许可协议的条款 @@ -4164,7 +4483,7 @@ function CreateShortcut(lnkpath, targetfile, appid) return ret; end function SetDesktopInit(inipath, section, key, value) - ret = File.Run(SessionVar.Expand ("%AppFolder%\\desktopini.exe"), "\"" .. inipath .. "\" \"" .. section .. "\" \"" .. key .. "\" \"" .. value .. "\"", "", SW_SHOWNORMAL, true); + ret = File.Run(SessionVar.Expand ("%AppFolder%\\desktopini.exe"), "\"" .. inipath .. "\" \"" .. section .. "\" \"" .. key .. "\" \"" .. value .. "\"", "", SW_HIDE, true); return ret; end startitemfolder = SessionVar.Expand ("%StartProgramsFolderCommon%\\%AppShortcutFolderName%"); @@ -4179,7 +4498,8 @@ SetDesktopInit (desktopini, ".ShellClassInfo", "ConfirmFileOp", 0); SetDesktopInit (desktopini, "LocalizedFileNames", "App Installer.lnk", SessionVar.Expand("@%AppFolder%\\appinstaller.exe,-300")); SetDesktopInit (desktopini, "LocalizedFileNames", "Settings.lnk", SessionVar.Expand("@%AppFolder%\\settings.exe,-200")); SetDesktopInit (desktopini, "LocalizedFileNames", "Update.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-103")); -SetDesktopInit (desktopini, "LocalizedFileNames", "Uninstaller.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-131")); +SetDesktopInit (desktopini, "LocalizedFileNames", "Uninstall.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-131")); +SetDesktopInit (desktopini, ".ShellClassInfo", "LocalizedResourceName", SessionVar.Expand("@%AppFolder%\\appinstaller.exe,-300")); DlgScrollingText.AppendLine(CTRL_SCROLLTEXT_BODY, "Updating system PATH..."); local appFolder = SessionVar.Expand("%AppFolder%"); @@ -6181,7 +6501,7 @@ g_HandleSystemReboot(); %ProductVer% -0.1.0.0 +0.2.0.0 1 diff --git a/others/Autosave/autosave_2025-12-07_22-40-28.suf b/others/Autosave/autosave_2025-12-09_20-05-41.suf similarity index 94% rename from others/Autosave/autosave_2025-12-07_22-40-28.suf rename to others/Autosave/autosave_2025-12-09_20-05-41.suf index afff745..2e6447b 100644 --- a/others/Autosave/autosave_2025-12-07_22-40-28.suf +++ b/others/Autosave/autosave_2025-12-09_20-05-41.suf @@ -687,7 +687,7 @@ %StartProgramsFolderCommon%\\%AppShortcutFolderName% Update -appinstaller update /autoupdate +appinstaller update /checkupdate 0 @@ -953,6 +953,152 @@ 0 0 + +0 +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release\PriFileFormat.dll.config +PriFileFormat.dll.config +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release +config +档案 + +1 +0 +%AppFolder% +1 +0 +0 +1000 +0 +0 +0 +0 +0 +0 +0 +0 + + + + + +0 + +0 +0 +0 +0 + +0 +0 +0 +1 +1 +0 +0 +0 +0 + +32768 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 + + + +All + +None + + +0 +0 +0 + + +0 +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release\priformatcli.dll.metagen +priformatcli.dll.metagen +E:\Profiles\Bruce\Documents\Visual Studio 2015\Projects\AppInstallerReset\Release +metagen +档案 + +1 +0 +%AppFolder% +1 +0 +0 +1000 +0 +0 +0 +0 +0 +0 +0 +0 + + + + + +0 + +0 +0 +0 +0 + +0 +0 +0 +1 +1 +0 +0 +0 +0 + +32768 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 + + + +All + +None + + +0 +0 +0 + @@ -2119,7 +2265,7 @@ end 1 1 0 -0 +1 0 1 0 @@ -2180,7 +2326,97 @@ end < &Back &Cancel &Help -Insert your license agreement text here... +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>End User License Agreement</title> + <style> + body { + /*font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ +-ms-overflow-style: -ms-autohiding-scrollbar; +box-sizing: border-box; + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> + +<body> + <h1>End User License Agreement</h1> + + <p>Please read this license agreement carefully before installing or using this software.</p> + + <h2>1. Copyright</h2> + <p>Copyright 漏 2025 Windows Modern. This software and its source code are protected by copyright law.</p> + + <h2>2. License</h2> + <p>This software is licensed under the MIT License. You are free to use, copy, modify, and distribute this software and its source code, including for commercial purposes, subject to the terms of the MIT License.</p> + + <h2>3. Third-Party Components</h2> + <p>This software includes the following third-party open source components, which are subject to their original licenses:</p> + <ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> + </ul> + + <h2>4. Disclaimer</h2> + <p>This software is provided "as is", without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be + liable for any damages arising from the use of this software, whether direct, indirect, incidental, or consequential.</p> + + <h2>5. Acceptance</h2> + <p>By installing or using this software, you agree to this license agreement. If you do not agree, do not install or use the software.</p> +<br> +</body> + +</html> I agree to the terms of this license agreement I do not agree to the terms of this license agreement @@ -2206,7 +2442,94 @@ end < 返回(&B) 取消(&C) 帮助(&H) -在此插入您的许可协议文本... +<!DOCTYPE html> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=GB2312"> + <title>最终用户许可协议</title> + <style> + body { + /*font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ +-ms-overflow-style: -ms-autohiding-scrollbar; +box-sizing: border-box; + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> +<body> +<h1>最终用户许可协议</h1> + +<p>请在安装或使用本软件前仔细阅读本许可协议。</p> + +<h2>一、版权声明</h2> +<p>Copyright &#169; 2025 Windows Modern. 本软件及其源码受版权法保护。</p> + +<h2>二、许可范围</h2> +<p>本软件遵循 MIT 许可协议。您可以在符合 MIT 许可条件下自由使用、复制、修改、分发本软件及其源码,包括商业用途。</p> + +<h2>三、第三方组件</h2> +<p>本软件包含以下第三方开源组件,使用这些组件受其原始许可证约束:</p> +<ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> +</ul> + +<h2>四、免责声明</h2> +<p>本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性、特定用途适用性和非侵权的保证。无论在何种情况下,作者或版权持有人均不对因使用本软件产生的任何直接、间接、偶然或特殊损害负责。</p> + +<h2>五、接受条款</h2> +<p>安装或使用本软件即表示您接受本许可协议。如果您不同意本协议,请不要安装或使用本软件。</p> +<br> +</body> +</html> + 我同意该许可协议的条款 我不同意该许可协议的条款 @@ -4164,13 +4487,13 @@ function CreateShortcut(lnkpath, targetfile, appid) return ret; end function SetDesktopInit(inipath, section, key, value) - ret = File.Run(SessionVar.Expand ("%AppFolder%\\desktopini.exe"), "\"" .. inipath .. "\" \"" .. section .. "\" \"" .. key .. "\" \"" .. value .. "\"", "", SW_SHOWNORMAL, true); + ret = File.Run(SessionVar.Expand ("%AppFolder%\\desktopini.exe"), "\"" .. inipath .. "\" \"" .. section .. "\" \"" .. key .. "\" \"" .. value .. "\"", "", SW_HIDE, true); return ret; end startitemfolder = SessionVar.Expand ("%StartProgramsFolderCommon%\\%AppShortcutFolderName%"); applnkpath = startitemfolder .. "\\App Installer.lnk"; setlnkpath = startitemfolder .. "\\Settings.lnk"; -desktopini = startitemfolder .. "\\desktop.ini"; +desktopini = startitemfolder .. ""; CreateShortcut (applnkpath, SessionVar.Expand("%AppFolder%\\appinstaller.exe"), "Microsoft.DesktopAppInstaller!App"); Registry.SetValue(HKEY_CURRENT_USER, "SOFTWARE\\Windows Modern\\App Installer", "AppInstallerLnk", applnkpath, REG_SZ); CreateShortcut (setlnkpath, SessionVar.Expand("%AppFolder%\\settings.exe"), "WindowsModern.PracticalToolsProject!Settings"); @@ -4179,7 +4502,8 @@ SetDesktopInit (desktopini, ".ShellClassInfo", "ConfirmFileOp", 0); SetDesktopInit (desktopini, "LocalizedFileNames", "App Installer.lnk", SessionVar.Expand("@%AppFolder%\\appinstaller.exe,-300")); SetDesktopInit (desktopini, "LocalizedFileNames", "Settings.lnk", SessionVar.Expand("@%AppFolder%\\settings.exe,-200")); SetDesktopInit (desktopini, "LocalizedFileNames", "Update.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-103")); -SetDesktopInit (desktopini, "LocalizedFileNames", "Uninstaller.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-131")); +SetDesktopInit (desktopini, "LocalizedFileNames", "Uninstall.lnk", SessionVar.Expand("@%AppFolder%\\reslib.dll,-131")); +SetDesktopInit (desktopini, ".ShellClassInfo", "LocalizedResourceName", SessionVar.Expand("@%AppFolder%\\appinstaller.exe,-300")); DlgScrollingText.AppendLine(CTRL_SCROLLTEXT_BODY, "Updating system PATH..."); local appFolder = SessionVar.Expand("%AppFolder%"); @@ -6181,7 +6505,7 @@ g_HandleSystemReboot(); %ProductVer% -0.1.0.0 +0.2.0.0 1 diff --git a/others/buildsetup.suf b/others/buildsetup.suf index 452f861..2e6447b 100644 --- a/others/buildsetup.suf +++ b/others/buildsetup.suf @@ -2265,7 +2265,7 @@ end 1 1 0 -0 +1 0 1 0 @@ -2326,7 +2326,97 @@ end < &Back &Cancel &Help -Insert your license agreement text here... +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>End User License Agreement</title> + <style> + body { + /*font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ +-ms-overflow-style: -ms-autohiding-scrollbar; +box-sizing: border-box; + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> + +<body> + <h1>End User License Agreement</h1> + + <p>Please read this license agreement carefully before installing or using this software.</p> + + <h2>1. Copyright</h2> + <p>Copyright 漏 2025 Windows Modern. This software and its source code are protected by copyright law.</p> + + <h2>2. License</h2> + <p>This software is licensed under the MIT License. You are free to use, copy, modify, and distribute this software and its source code, including for commercial purposes, subject to the terms of the MIT License.</p> + + <h2>3. Third-Party Components</h2> + <p>This software includes the following third-party open source components, which are subject to their original licenses:</p> + <ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> + </ul> + + <h2>4. Disclaimer</h2> + <p>This software is provided "as is", without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be + liable for any damages arising from the use of this software, whether direct, indirect, incidental, or consequential.</p> + + <h2>5. Acceptance</h2> + <p>By installing or using this software, you agree to this license agreement. If you do not agree, do not install or use the software.</p> +<br> +</body> + +</html> I agree to the terms of this license agreement I do not agree to the terms of this license agreement @@ -2352,7 +2442,94 @@ end < 返回(&B) 取消(&C) 帮助(&H) -在此插入您的许可协议文本... +<!DOCTYPE html> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=GB2312"> + <title>最终用户许可协议</title> + <style> + body { + /*font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;*/ + font-size: 9pt; + /*color: #333333; + background-color: #ffffff;*/ +-ms-overflow-style: -ms-autohiding-scrollbar; +box-sizing: border-box; + } + + h1 { + font-size: 12.5pt; + font-weight: bold; + margin-bottom: 5px; +text-align: center; + } + + h2 { + font-size: 10.75pt; + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; + } + + p { + margin: 5px 0; + } + + ul, + ol { + margin: 5px 0; + padding: 0; + } + + a { + color: #1a73e8; + text-decoration: underline; + } + + a:hover { + color: #0b59d1; + text-decoration: none; + } + + strong { + font-weight: bold; + } + + em { + font-style: italic; + } + </style> +</head> +<body> +<h1>最终用户许可协议</h1> + +<p>请在安装或使用本软件前仔细阅读本许可协议。</p> + +<h2>一、版权声明</h2> +<p>Copyright &#169; 2025 Windows Modern. 本软件及其源码受版权法保护。</p> + +<h2>二、许可范围</h2> +<p>本软件遵循 MIT 许可协议。您可以在符合 MIT 许可条件下自由使用、复制、修改、分发本软件及其源码,包括商业用途。</p> + +<h2>三、第三方组件</h2> +<p>本软件包含以下第三方开源组件,使用这些组件受其原始许可证约束:</p> +<ul> + <li>PriTools (Apache License 2.0) - <a href="https://github.com/chausner/PriTools" target="_blank">https://github.com/chausner/PriTools</a></li> + <li>pugixml (MIT License) - <a href="https://pugixml.org/" target="_blank">https://pugixml.org/</a></li> + <li>RapidJSON (MIT License) - <a href="https://rapidjson.org/" target="_blank">https://rapidjson.org/</a></li> + <li>WinJS (MIT License) - <a href="https://github.com/winjs/winjs" target="_blank">https://github.com/winjs/winjs</a></li> + <li>markdown.js (MIT License) - <a href="https://github.com/evilstreak/markdown-js" target="_blank">https://github.com/evilstreak/markdown-js</a></li> +</ul> + +<h2>四、免责声明</h2> +<p>本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性、特定用途适用性和非侵权的保证。无论在何种情况下,作者或版权持有人均不对因使用本软件产生的任何直接、间接、偶然或特殊损害负责。</p> + +<h2>五、接受条款</h2> +<p>安装或使用本软件即表示您接受本许可协议。如果您不同意本协议,请不要安装或使用本软件。</p> +<br> +</body> +</html> + 我同意该许可协议的条款 我不同意该许可协议的条款 diff --git a/pkgmgr/pkgmgr.rc b/pkgmgr/pkgmgr.rc index 8f60aa3..8a8a1ab 100644 Binary files a/pkgmgr/pkgmgr.rc and b/pkgmgr/pkgmgr.rc differ diff --git a/pkgread/pkgread.rc b/pkgread/pkgread.rc index bb4eee9..051d6aa 100644 Binary files a/pkgread/pkgread.rc and b/pkgread/pkgread.rc differ diff --git a/reslib/reslib.rc b/reslib/reslib.rc index 21838fc..47fcbe6 100644 Binary files a/reslib/reslib.rc and b/reslib/reslib.rc differ diff --git a/reslib/resource.h b/reslib/resource.h index 2251fdd..87f4558 100644 Binary files a/reslib/resource.h and b/reslib/resource.h differ diff --git a/settings/main.cpp b/settings/main.cpp index f632617..fabc2a5 100644 --- a/settings/main.cpp +++ b/settings/main.cpp @@ -574,6 +574,97 @@ bool KillProcessByFilePath ( CloseHandle (hSnap); return killed; } +static std::wstring QueryVersionString (const BYTE *data, const std::wstring &langCode, + const wchar_t *key) +{ + wchar_t query [256] = {0}; + wsprintfW (query, L"\\StringFileInfo\\%s\\%s", langCode.c_str (), key); + LPWSTR value = nullptr; + UINT len = 0; + if (VerQueryValueW (data, query, (LPVOID *)&value, &len) && value) + return std::wstring (value ? value : L"", len); + return L""; +} +rapidjson::Document GetFileVersionAsJson (const std::wstring &filePath) +{ + rapidjson::Document doc; + doc.SetObject (); + auto &alloc = doc.GetAllocator (); + DWORD dummy = 0; + DWORD size = GetFileVersionInfoSizeW (filePath.c_str (), &dummy); + if (size == 0) + { + doc.AddMember ("error", "No version info", alloc); + return doc; + } + std::vector data (size); + if (!GetFileVersionInfoW (filePath.c_str (), 0, size, data.data ())) + { + doc.AddMember ("error", "GetFileVersionInfoW failed", alloc); + return doc; + } + struct LANGANDCODEPAGE + { + WORD wLanguage; + WORD wCodePage; + }; + LANGANDCODEPAGE *lpTranslate = nullptr; + UINT cbTranslate = 0; + if (!VerQueryValueW (data.data (), + L"\\VarFileInfo\\Translation", + (LPVOID *)&lpTranslate, + &cbTranslate)) + { + doc.AddMember ("error", "No Translation", alloc); + return doc; + } + wchar_t langCode [20] = {}; + wsprintfW (langCode, L"%04x%04x", + lpTranslate [0].wLanguage, + lpTranslate [0].wCodePage); + std::wstring lc = langCode; + const wchar_t *keys [] = + { + L"CompanyName", + L"FileDescription", + L"FileVersion", + L"InternalName", + L"OriginalFilename", + L"ProductName", + L"ProductVersion", + L"LegalCopyright" + }; + for (auto key : keys) + { + std::wstring val = QueryVersionString (data.data (), lc, key); + if (!val.empty ()) + { + rapidjson::Value k; + k.SetString (WStringToString (key, CP_UTF8).c_str (), alloc); + rapidjson::Value v; + v.SetString (WStringToString (val, CP_UTF8).c_str (), alloc); + doc.AddMember (k, v, alloc); + } + } + VS_FIXEDFILEINFO *ffi = nullptr; + UINT ffiLen = 0; + if (VerQueryValueW (data.data (), L"\\", (LPVOID *)&ffi, &ffiLen)) + { + if (ffi && ffiLen) + { + wchar_t ver [64]; + wsprintfW (ver, L"%u.%u.%u.%u", + HIWORD (ffi->dwFileVersionMS), + LOWORD (ffi->dwFileVersionMS), + HIWORD (ffi->dwFileVersionLS), + LOWORD (ffi->dwFileVersionLS)); + doc.AddMember ("FileVersionRaw", + rapidjson::Value ().SetString (WStringToString (ver, CP_UTF8).c_str (), alloc), + alloc); + } + } + return doc; +} [ComVisible (true)] public ref class SplashForm: public System::Windows::Forms::Form @@ -946,11 +1037,22 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form, public IScrip property _I_Download ^Download { _I_Download ^get () { return download; }} property _I_Process ^Process { _I_Process ^get () { return proc; }} property _I_ResourcePri ^WinJsStringRes { _I_ResourcePri ^get () { return winjs_res; }} + property _I_ResourcePri ^StringResources { _I_ResourcePri ^get () { return winjs_res; }} String ^FormatDateTime (String ^fmt, String ^jsDate) { return FormatString (fmt, Convert::ToDateTime (jsDate)); } property String ^CmdArgs { String ^get () { return CStringToMPString (StringArrayToJson (g_cmdargs)); }} property bool Jump1 { bool get () { return hasjump1; } void set (bool value) { hasjump1 = value; } } property bool Jump2 { bool get () { return hasjump2; } void set (bool value) { hasjump2 = value; } } property bool Exec1 { bool get () { return hasexec; } void set (bool value) { hasexec = value; } } + property double TBProgress { void set (double value) { wndinst->TProgressPercent = value; }} + property DWORD TBState { void set (DWORD value) { wndinst->TProgressState = (TBPFLAG)value; }} + String ^GetVersionInfoToJSON (String ^filepath) + { + auto doc = GetFileVersionAsJson (MPStringToStdW (filepath)); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer (buffer); + doc.Accept (writer); + return CStringToMPString (StringToWString (buffer.GetString (), CP_UTF8)); + } void CloseWindow () { if (wndinst && wndinst->IsHandleCreated) wndinst->Close (); @@ -1243,6 +1345,9 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form, public IScrip web2->ExecWB (OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, &v, nullptr); } } + // 只能设置。0 - 100 + property double TProgressPercent { void set (double progressPercent) { if (taskbar) taskbar->SetProgressValue (InvokeGetHWND (), progressPercent * 100, 100 * 100); }} + property TBPFLAG TProgressState { void set (TBPFLAG state) { if (taskbar) taskbar->SetProgressState (InvokeGetHWND (), state); }} ~MainHtmlWnd () { if (taskbar) taskbar->Release (); diff --git a/settings/settings.rc b/settings/settings.rc index 442c4a5..0c43bdc 100644 Binary files a/settings/settings.rc and b/settings/settings.rc differ diff --git a/shared/config.ini b/shared/config.ini index 463fcbe..0a03759 100644 Binary files a/shared/config.ini and b/shared/config.ini differ diff --git a/shared/html/libs/markdown.js b/shared/html/libs/markdown.js index d365cfa..987ecfc 100644 --- a/shared/html/libs/markdown.js +++ b/shared/html/libs/markdown.js @@ -1,3 +1,4 @@ +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) { return typeof o; } : function(o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } // Released under MIT license // Copyright (c) 2009-2010 Dominic Baggott // Copyright (c) 2009-2010 Ash Berlin @@ -5,1721 +6,1534 @@ /*jshint browser:true, devel:true */ -(function( expose ) { - -/** - * class Markdown - * - * Markdown processing in Javascript done right. We have very particular views - * on what constitutes 'right' which include: - * - * - produces well-formed HTML (this means that em and strong nesting is - * important) - * - * - has an intermediate representation to allow processing of parsed data (We - * in fact have two, both as [JsonML]: a markdown tree and an HTML tree). - * - * - is easily extensible to add new dialects without having to rewrite the - * entire parsing mechanics - * - * - has a good test suite - * - * This implementation fulfills all of these (except that the test suite could - * do with expanding to automatically run all the fixtures from other Markdown - * implementations.) - * - * ##### Intermediate Representation - * - * *TODO* Talk about this :) Its JsonML, but document the node names we use. - * - * [JsonML]: http://jsonml.org/ "JSON Markup Language" - **/ -var Markdown = expose.Markdown = function(dialect) { - switch (typeof dialect) { - case "undefined": - this.dialect = Markdown.dialects.Gruber; - break; - case "object": - this.dialect = dialect; - break; - default: - if ( dialect in Markdown.dialects ) { - this.dialect = Markdown.dialects[dialect]; - } - else { - throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); - } - break; - } - this.em_state = []; - this.strong_state = []; - this.debug_indent = ""; -}; - -/** - * parse( markdown, [dialect] ) -> JsonML - * - markdown (String): markdown string to parse - * - dialect (String | Dialect): the dialect to use, defaults to gruber - * - * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. - **/ -expose.parse = function( source, dialect ) { - // dialect will default if undefined - var md = new Markdown( dialect ); - return md.toTree( source ); -}; - -/** - * toHTML( markdown, [dialect] ) -> String - * toHTML( md_tree ) -> String - * - markdown (String): markdown string to parse - * - md_tree (Markdown.JsonML): parsed markdown tree - * - * Take markdown (either as a string or as a JsonML tree) and run it through - * [[toHTMLTree]] then turn it into a well-formated HTML fragment. - **/ -expose.toHTML = function toHTML( source , dialect , options ) { - var input = expose.toHTMLTree( source , dialect , options ); - - return expose.renderJsonML( input ); -}; - -/** - * toHTMLTree( markdown, [dialect] ) -> JsonML - * toHTMLTree( md_tree ) -> JsonML - * - markdown (String): markdown string to parse - * - dialect (String | Dialect): the dialect to use, defaults to gruber - * - md_tree (Markdown.JsonML): parsed markdown tree - * - * Turn markdown into HTML, represented as a JsonML tree. If a string is given - * to this function, it is first parsed into a markdown tree by calling - * [[parse]]. - **/ -expose.toHTMLTree = function toHTMLTree( input, dialect , options ) { - // convert string input to an MD tree - if ( typeof input ==="string" ) input = this.parse( input, dialect ); - - // Now convert the MD tree to an HTML tree - - // remove references from the tree - var attrs = extract_attr( input ), - refs = {}; - - if ( attrs && attrs.references ) { - refs = attrs.references; - } - - var html = convert_tree_to_html( input, refs , options ); - merge_text_nodes( html ); - return html; -}; - -// For Spidermonkey based engines -function mk_block_toSource() { - return "Markdown.mk_block( " + - uneval(this.toString()) + - ", " + - uneval(this.trailing) + - ", " + - uneval(this.lineNumber) + - " )"; -} - -// node -function mk_block_inspect() { - var util = require("util"); - return "Markdown.mk_block( " + - util.inspect(this.toString()) + - ", " + - util.inspect(this.trailing) + - ", " + - util.inspect(this.lineNumber) + - " )"; - -} - -var mk_block = Markdown.mk_block = function(block, trail, line) { - // Be helpful for default case in tests. - if ( arguments.length == 1 ) trail = "\n\n"; - - var s = new String(block); - s.trailing = trail; - // To make it clear its not just a string - s.inspect = mk_block_inspect; - s.toSource = mk_block_toSource; - - if ( line != undefined ) - s.lineNumber = line; - - return s; -}; - -function count_lines( str ) { - var n = 0, i = -1; - while ( ( i = str.indexOf("\n", i + 1) ) !== -1 ) n++; - return n; -} - -// Internal - split source into rough blocks -Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) { - input = input.replace(/(\r\n|\n|\r)/g, "\n"); - // [\s\S] matches _anything_ (newline or space) - // [^] is equivalent but doesn't work in IEs. - var re = /([\s\S]+?)($|\n#|\n(?:\s*\n|$)+)/g, - blocks = [], - m; - - var line_no = 1; - - if ( ( m = /^(\s*\n)/.exec(input) ) != null ) { - // skip (but count) leading blank lines - line_no += count_lines( m[0] ); - re.lastIndex = m[0].length; - } - - while ( ( m = re.exec(input) ) !== null ) { - if (m[2] == "\n#") { - m[2] = "\n"; - re.lastIndex--; - } - blocks.push( mk_block( m[1], m[2], line_no ) ); - line_no += count_lines( m[0] ); - } - - return blocks; -}; - -/** - * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] - * - block (String): the block to process - * - next (Array): the following blocks - * - * Process `block` and return an array of JsonML nodes representing `block`. - * - * It does this by asking each block level function in the dialect to process - * the block until one can. Succesful handling is indicated by returning an - * array (with zero or more JsonML nodes), failure by a false value. - * - * Blocks handlers are responsible for calling [[Markdown#processInline]] - * themselves as appropriate. - * - * If the blocks were split incorrectly or adjacent blocks need collapsing you - * can adjust `next` in place using shift/splice etc. - * - * If any of this default behaviour is not right for the dialect, you can - * define a `__call__` method on the dialect that will get invoked to handle - * the block processing. - */ -Markdown.prototype.processBlock = function processBlock( block, next ) { - var cbs = this.dialect.block, - ord = cbs.__order__; - - if ( "__call__" in cbs ) { - return cbs.__call__.call(this, block, next); - } - - for ( var i = 0; i < ord.length; i++ ) { - //D:this.debug( "Testing", ord[i] ); - var res = cbs[ ord[i] ].call( this, block, next ); - if ( res ) { - //D:this.debug(" matched"); - if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) ) - this.debug(ord[i], "didn't return a proper array"); - //D:this.debug( "" ); - return res; - } - } - - // Uhoh! no match! Should we throw an error? - return []; -}; - -Markdown.prototype.processInline = function processInline( block ) { - return this.dialect.inline.__call__.call( this, String( block ) ); -}; - -/** - * Markdown#toTree( source ) -> JsonML - * - source (String): markdown source to parse - * - * Parse `source` into a JsonML tree representing the markdown document. - **/ -// custom_tree means set this.tree to `custom_tree` and restore old value on return -Markdown.prototype.toTree = function toTree( source, custom_root ) { - var blocks = source instanceof Array ? source : this.split_blocks( source ); - - // Make tree a member variable so its easier to mess with in extensions - var old_tree = this.tree; - try { - this.tree = custom_root || this.tree || [ "markdown" ]; - - blocks: - while ( blocks.length ) { - var b = this.processBlock( blocks.shift(), blocks ); - - // Reference blocks and the like won't return any content - if ( !b.length ) continue blocks; - - this.tree.push.apply( this.tree, b ); - } - return this.tree; - } - finally { - if ( custom_root ) { - this.tree = old_tree; - } - } -}; - -// Noop by default -Markdown.prototype.debug = function () { - var args = Array.prototype.slice.call( arguments); - args.unshift(this.debug_indent); - if ( typeof print !== "undefined" ) - print.apply( print, args ); - if ( typeof console !== "undefined" && typeof console.log !== "undefined" ) - console.log.apply( null, args ); -} - -Markdown.prototype.loop_re_over_block = function( re, block, cb ) { - // Dont use /g regexps with this - var m, - b = block.valueOf(); - - while ( b.length && (m = re.exec(b) ) != null ) { - b = b.substr( m[0].length ); - cb.call(this, m); - } - return b; -}; - -/** - * Markdown.dialects - * - * Namespace of built-in dialects. - **/ -Markdown.dialects = {}; - -/** - * Markdown.dialects.Gruber - * - * The default dialect that follows the rules set out by John Gruber's - * markdown.pl as closely as possible. Well actually we follow the behaviour of - * that script which in some places is not exactly what the syntax web page - * says. - **/ -Markdown.dialects.Gruber = { - block: { - atxHeader: function atxHeader( block, next ) { - var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ ); - - if ( !m ) return undefined; - - var header = [ "header", { level: m[ 1 ].length } ]; - Array.prototype.push.apply(header, this.processInline(m[ 2 ])); - - if ( m[0].length < block.length ) - next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); - - return [ header ]; - }, - - setextHeader: function setextHeader( block, next ) { - var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ ); - - if ( !m ) return undefined; - - var level = ( m[ 2 ] === "=" ) ? 1 : 2; - var header = [ "header", { level : level }, m[ 1 ] ]; - - if ( m[0].length < block.length ) - next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); - - return [ header ]; - }, - - code: function code( block, next ) { - // | Foo - // |bar - // should be a code block followed by a paragraph. Fun - // - // There might also be adjacent code block to merge. - - var ret = [], - re = /^(?: {0,3}\t| {4})(.*)\n?/, - lines; - - // 4 spaces + content - if ( !block.match( re ) ) return undefined; - - block_search: - do { - // Now pull out the rest of the lines - var b = this.loop_re_over_block( - re, block.valueOf(), function( m ) { ret.push( m[1] ); } ); - - if ( b.length ) { - // Case alluded to in first comment. push it back on as a new block - next.unshift( mk_block(b, block.trailing) ); - break block_search; - } - else if ( next.length ) { - // Check the next block - it might be code too - if ( !next[0].match( re ) ) break block_search; - - // Pull how how many blanks lines follow - minus two to account for .join - ret.push ( block.trailing.replace(/[^\n]/g, "").substring(2) ); - - block = next.shift(); - } - else { - break block_search; - } - } while ( true ); - - return [ [ "code_block", ret.join("\n") ] ]; - }, - - horizRule: function horizRule( block, next ) { - // this needs to find any hr in the block to handle abutting blocks - var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ ); - - if ( !m ) { - return undefined; - } - - var jsonml = [ [ "hr" ] ]; - - // if there's a leading abutting block, process it - if ( m[ 1 ] ) { - jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) ); - } - - // if there's a trailing abutting block, stick it into next - if ( m[ 3 ] ) { - next.unshift( mk_block( m[ 3 ] ) ); - } - - return jsonml; - }, - - // There are two types of lists. Tight and loose. Tight lists have no whitespace - // between the items (and result in text just in the
  • ) and loose lists, - // which have an empty line between list items, resulting in (one or more) - // paragraphs inside the
  • . - // - // There are all sorts weird edge cases about the original markdown.pl's - // handling of lists: - // - // * Nested lists are supposed to be indented by four chars per level. But - // if they aren't, you can get a nested list by indenting by less than - // four so long as the indent doesn't match an indent of an existing list - // item in the 'nest stack'. - // - // * The type of the list (bullet or number) is controlled just by the - // first item at the indent. Subsequent changes are ignored unless they - // are for nested lists - // - lists: (function( ) { - // Use a closure to hide a few variables. - var any_list = "[*+-]|\\d+\\.", - bullet_list = /[*+-]/, - number_list = /\d+\./, - // Capture leading indent as it matters for determining nested lists. - is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ), - indent_re = "(?: {0,3}\\t| {4})"; - - // TODO: Cache this regexp for certain depths. - // Create a regexp suitable for matching an li for a given stack depth - function regex_for_depth( depth ) { - - return new RegExp( - // m[1] = indent, m[2] = list_type - "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" + - // m[3] = cont - "(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})" - ); - } - function expand_tab( input ) { - return input.replace( / {0,3}\t/g, " " ); - } - - // Add inline content `inline` to `li`. inline comes from processInline - // so is an array of content - function add(li, loose, inline, nl) { - if ( loose ) { - li.push( [ "para" ].concat(inline) ); - return; - } - // Hmmm, should this be any block level element or just paras? - var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para" - ? li[li.length -1] - : li; - - // If there is already some content in this list, add the new line in - if ( nl && li.length > 1 ) inline.unshift(nl); - - for ( var i = 0; i < inline.length; i++ ) { - var what = inline[i], - is_str = typeof what == "string"; - if ( is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) { - add_to[ add_to.length-1 ] += what; - } - else { - add_to.push( what ); - } - } - } - - // contained means have an indent greater than the current one. On - // *every* line in the block - function get_contained_blocks( depth, blocks ) { - - var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ), - replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"), - ret = []; - - while ( blocks.length > 0 ) { - if ( re.exec( blocks[0] ) ) { - var b = blocks.shift(), - // Now remove that indent - x = b.replace( replace, ""); - - ret.push( mk_block( x, b.trailing, b.lineNumber ) ); - } - else { - break; - } - } - return ret; - } - - // passed to stack.forEach to turn list items up the stack into paras - function paragraphify(s, i, stack) { - var list = s.list; - var last_li = list[list.length-1]; - - if ( last_li[1] instanceof Array && last_li[1][0] == "para" ) { - return; - } - if ( i + 1 == stack.length ) { - // Last stack frame - // Keep the same array, but replace the contents - last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ) ); - } - else { - var sublist = last_li.pop(); - last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ), sublist ); - } - } - - // The matcher function - return function( block, next ) { - var m = block.match( is_list_re ); - if ( !m ) return undefined; - - function make_list( m ) { - var list = bullet_list.exec( m[2] ) - ? ["bulletlist"] - : ["numberlist"]; - - stack.push( { list: list, indent: m[1] } ); - return list; - } - - - var stack = [], // Stack of lists for nesting. - list = make_list( m ), - last_li, - loose = false, - ret = [ stack[0].list ], - i; - - // Loop to search over block looking for inner block elements and loose lists - loose_search: - while ( true ) { - // Split into lines preserving new lines at end of line - var lines = block.split( /(?=\n)/ ); - - // We have to grab all lines for a li and call processInline on them - // once as there are some inline things that can span lines. - var li_accumulate = ""; - - // Loop over the lines in this block looking for tight lists. - tight_search: - for ( var line_no = 0; line_no < lines.length; line_no++ ) { - var nl = "", - l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; }); - - // TODO: really should cache this - var line_re = regex_for_depth( stack.length ); - - m = l.match( line_re ); - //print( "line:", uneval(l), "\nline match:", uneval(m) ); - - // We have a list item - if ( m[1] !== undefined ) { - // Process the previous list item, if any - if ( li_accumulate.length ) { - add( last_li, loose, this.processInline( li_accumulate ), nl ); - // Loose mode will have been dealt with. Reset it - loose = false; - li_accumulate = ""; - } - - m[1] = expand_tab( m[1] ); - var wanted_depth = Math.floor(m[1].length/4)+1; - //print( "want:", wanted_depth, "stack:", stack.length); - if ( wanted_depth > stack.length ) { - // Deep enough for a nested list outright - //print ( "new nested list" ); - list = make_list( m ); - last_li.push( list ); - last_li = list[1] = [ "listitem" ]; - } - else { - // We aren't deep enough to be strictly a new level. This is - // where Md.pl goes nuts. If the indent matches a level in the - // stack, put it there, else put it one deeper then the - // wanted_depth deserves. - var found = false; - for ( i = 0; i < stack.length; i++ ) { - if ( stack[ i ].indent != m[1] ) continue; - list = stack[ i ].list; - stack.splice( i+1, stack.length - (i+1) ); - found = true; - break; +(function(expose) { + /** + * class Markdown + * + * Markdown processing in Javascript done right. We have very particular views + * on what constitutes 'right' which include: + * + * - produces well-formed HTML (this means that em and strong nesting is + * important) + * + * - has an intermediate representation to allow processing of parsed data (We + * in fact have two, both as [JsonML]: a markdown tree and an HTML tree). + * + * - is easily extensible to add new dialects without having to rewrite the + * entire parsing mechanics + * + * - has a good test suite + * + * This implementation fulfills all of these (except that the test suite could + * do with expanding to automatically run all the fixtures from other Markdown + * implementations.) + * + * ##### Intermediate Representation + * + * *TODO* Talk about this :) Its JsonML, but document the node names we use. + * + * [JsonML]: http://jsonml.org/ "JSON Markup Language" + **/ + var Markdown = expose.Markdown = function(dialect) { + switch (_typeof(dialect)) { + case "undefined": + this.dialect = Markdown.dialects.Gruber; + break; + case "object": + this.dialect = dialect; + break; + default: + if (dialect in Markdown.dialects) { + this.dialect = Markdown.dialects[dialect]; + } else { + throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); } - - if (!found) { - //print("not found. l:", uneval(l)); - wanted_depth++; - if ( wanted_depth <= stack.length ) { - stack.splice(wanted_depth, stack.length - wanted_depth); - //print("Desired depth now", wanted_depth, "stack:", stack.length); - list = stack[wanted_depth-1].list; - //print("list:", uneval(list) ); - } - else { - //print ("made new stack for messy indent"); - list = make_list(m); - last_li.push(list); - } - } - - //print( uneval(list), "last", list === stack[stack.length-1].list ); - last_li = [ "listitem" ]; - list.push(last_li); - } // end depth of shenegains - nl = ""; - } - - // Add content - if ( l.length > m[0].length ) { - li_accumulate += nl + l.substr( m[0].length ); - } - } // tight_search - - if ( li_accumulate.length ) { - add( last_li, loose, this.processInline( li_accumulate ), nl ); - // Loose mode will have been dealt with. Reset it - loose = false; - li_accumulate = ""; - } - - // Look at the next block - we might have a loose list. Or an extra - // paragraph for the current li - var contained = get_contained_blocks( stack.length, next ); - - // Deal with code blocks or properly nested lists - if ( contained.length > 0 ) { - // Make sure all listitems up the stack are paragraphs - forEach( stack, paragraphify, this); - - last_li.push.apply( last_li, this.toTree( contained, [] ) ); - } - - var next_block = next[0] && next[0].valueOf() || ""; - - if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) { - block = next.shift(); - - // Check for an HR following a list: features/lists/hr_abutting - var hr = this.dialect.block.horizRule( block, next ); - - if ( hr ) { - ret.push.apply(ret, hr); - break; - } - - // Make sure all listitems up the stack are paragraphs - forEach( stack, paragraphify, this); - - loose = true; - continue loose_search; - } - break; - } // loose_search - - return ret; - }; - })(), - - blockquote: function blockquote( block, next ) { - if ( !block.match( /^>/m ) ) - return undefined; - - var jsonml = []; - - // separate out the leading abutting block, if any. I.e. in this case: - // - // a - // > b - // - if ( block[ 0 ] != ">" ) { - var lines = block.split( /\n/ ), - prev = [], - line_no = block.lineNumber; - - // keep shifting lines until you find a crotchet - while ( lines.length && lines[ 0 ][ 0 ] != ">" ) { - prev.push( lines.shift() ); - line_no++; + break; } + this.em_state = []; + this.strong_state = []; + this.debug_indent = ""; + }; - var abutting = mk_block( prev.join( "\n" ), "\n", block.lineNumber ); - jsonml.push.apply( jsonml, this.processBlock( abutting, [] ) ); - // reassemble new block of just block quotes! - block = mk_block( lines.join( "\n" ), block.trailing, line_no ); - } + /** + * parse( markdown, [dialect] ) -> JsonML + * - markdown (String): markdown string to parse + * - dialect (String | Dialect): the dialect to use, defaults to gruber + * + * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. + **/ + expose.parse = function(source, dialect) { + // dialect will default if undefined + var md = new Markdown(dialect); + return md.toTree(source); + }; + /** + * toHTML( markdown, [dialect] ) -> String + * toHTML( md_tree ) -> String + * - markdown (String): markdown string to parse + * - md_tree (Markdown.JsonML): parsed markdown tree + * + * Take markdown (either as a string or as a JsonML tree) and run it through + * [[toHTMLTree]] then turn it into a well-formated HTML fragment. + **/ + expose.toHTML = function toHTML(source, dialect, options) { + var input = expose.toHTMLTree(source, dialect, options); + return expose.renderJsonML(input); + }; - // if the next block is also a blockquote merge it in - while ( next.length && next[ 0 ][ 0 ] == ">" ) { - var b = next.shift(); - block = mk_block( block + block.trailing + b, b.trailing, block.lineNumber ); - } + /** + * toHTMLTree( markdown, [dialect] ) -> JsonML + * toHTMLTree( md_tree ) -> JsonML + * - markdown (String): markdown string to parse + * - dialect (String | Dialect): the dialect to use, defaults to gruber + * - md_tree (Markdown.JsonML): parsed markdown tree + * + * Turn markdown into HTML, represented as a JsonML tree. If a string is given + * to this function, it is first parsed into a markdown tree by calling + * [[parse]]. + **/ + expose.toHTMLTree = function toHTMLTree(input, dialect, options) { + // convert string input to an MD tree + if (typeof input === "string") input = this.parse(input, dialect); - // Strip off the leading "> " and re-process as a block. - var input = block.replace( /^> ?/gm, "" ), - old_tree = this.tree, - processedBlock = this.toTree( input, [ "blockquote" ] ), - attr = extract_attr( processedBlock ); + // Now convert the MD tree to an HTML tree - // If any link references were found get rid of them - if ( attr && attr.references ) { - delete attr.references; - // And then remove the attribute object if it's empty - if ( isEmpty( attr ) ) { - processedBlock.splice( 1, 1 ); + // remove references from the tree + var attrs = extract_attr(input), + refs = {}; + if (attrs && attrs.references) { + refs = attrs.references; } - } - - jsonml.push( processedBlock ); - return jsonml; - }, - - referenceDefn: function referenceDefn( block, next) { - var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; - // interesting matches are [ , ref_id, url, , title, title ] - - if ( !block.match(re) ) - return undefined; - - // make an attribute node if it doesn't exist - if ( !extract_attr( this.tree ) ) { - this.tree.splice( 1, 0, {} ); - } - - var attrs = extract_attr( this.tree ); - - // make a references hash if it doesn't exist - if ( attrs.references === undefined ) { - attrs.references = {}; - } - - var b = this.loop_re_over_block(re, block, function( m ) { - - if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" ) - m[2] = m[2].substring( 1, m[2].length - 1 ); - - var ref = attrs.references[ m[1].toLowerCase() ] = { - href: m[2] - }; - - if ( m[4] !== undefined ) - ref.title = m[4]; - else if ( m[5] !== undefined ) - ref.title = m[5]; - - } ); - - if ( b.length ) - next.unshift( mk_block( b, block.trailing ) ); - - return []; - }, - - para: function para( block, next ) { - // everything's a para! - return [ ["para"].concat( this.processInline( block ) ) ]; - } - } -}; - -Markdown.dialects.Gruber.inline = { - - __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) { - var m, - res, - lastIndex = 0; - - patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; - var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" ); - - m = re.exec( text ); - if (!m) { - // Just boring text - return [ text.length, text ]; - } - else if ( m[1] ) { - // Some un-interesting text matched. Return that first - return [ m[1].length, m[1] ]; - } - - var res; - if ( m[2] in this.dialect.inline ) { - res = this.dialect.inline[ m[2] ].call( - this, - text.substr( m.index ), m, previous_nodes || [] ); - } - // Default for now to make dev easier. just slurp special and output it. - res = res || [ m[2].length, m[2] ]; - return res; - }, - - __call__: function inline( text, patterns ) { - - var out = [], - res; - - function add(x) { - //D:self.debug(" adding output", uneval(x)); - if ( typeof x == "string" && typeof out[out.length-1] == "string" ) - out[ out.length-1 ] += x; - else - out.push(x); - } - - while ( text.length > 0 ) { - res = this.dialect.inline.__oneElement__.call(this, text, patterns, out ); - text = text.substr( res.shift() ); - forEach(res, add ) - } - - return out; - }, - - // These characters are intersting elsewhere, so have rules for them so that - // chunks of plain text blocks don't include them - "]": function () {}, - "}": function () {}, - - __escape__ : /^\\[\\`\*_{}\[\]()#\+.!\-]/, - - "\\": function escaped( text ) { - // [ length of input processed, node/children to add... ] - // Only esacape: \ ` * _ { } [ ] ( ) # * + - . ! - if ( this.dialect.inline.__escape__.exec( text ) ) - return [ 2, text.charAt( 1 ) ]; - else - // Not an esacpe - return [ 1, "\\" ]; - }, - - "![": function image( text ) { - - // Unlike images, alt text is plain text only. no other elements are - // allowed in there - - // ![Alt text](/path/to/img.jpg "Optional title") - // 1 2 3 4 <--- captures - var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ ); - - if ( m ) { - if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" ) - m[2] = m[2].substring( 1, m[2].length - 1 ); - - m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0]; - - var attrs = { alt: m[1], href: m[2] || "" }; - if ( m[4] !== undefined) - attrs.title = m[4]; - - return [ m[0].length, [ "img", attrs ] ]; - } - - // ![Alt text][id] - m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ ); - - if ( m ) { - // We can't check if the reference is known here as it likely wont be - // found till after. Check it in md tree->hmtl tree conversion - return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ]; - } - - // Just consume the '![' - return [ 2, "![" ]; - }, - - "[": function link( text ) { - - var orig = String(text); - // Inline content is possible inside `link text` - var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" ); - - // No closing ']' found. Just consume the [ - if ( !res ) return [ 1, "[" ]; - - var consumed = 1 + res[ 0 ], - children = res[ 1 ], - link, - attrs; - - // At this point the first [...] has been parsed. See what follows to find - // out which kind of link we are (reference or direct url) - text = text.substr( consumed ); - - // [link text](/path/to/img.jpg "Optional title") - // 1 2 3 <--- captures - // This will capture up to the last paren in the block. We then pull - // back based on if there a matching ones in the url - // ([here](/url/(test)) - // The parens have to be balanced - var m = text.match( /^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ ); - if ( m ) { - var url = m[1]; - consumed += m[0].length; - - if ( url && url[0] == "<" && url[url.length-1] == ">" ) - url = url.substring( 1, url.length - 1 ); - - // If there is a title we don't have to worry about parens in the url - if ( !m[3] ) { - var open_parens = 1; // One open that isn't in the capture - for ( var len = 0; len < url.length; len++ ) { - switch ( url[len] ) { - case "(": - open_parens++; - break; - case ")": - if ( --open_parens == 0) { - consumed -= url.length - len; - url = url.substring(0, len); - } - break; - } - } - } - - // Process escapes only - url = this.dialect.inline.__call__.call( this, url, /\\/ )[0]; - - attrs = { href: url || "" }; - if ( m[3] !== undefined) - attrs.title = m[3]; - - link = [ "link", attrs ].concat( children ); - return [ consumed, link ]; - } - - // [Alt text][id] - // [Alt text] [id] - m = text.match( /^\s*\[(.*?)\]/ ); - - if ( m ) { - - consumed += m[ 0 ].length; - - // [links][] uses links as its reference - attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) }; - - link = [ "link_ref", attrs ].concat( children ); - - // We can't check if the reference is known here as it likely wont be - // found till after. Check it in md tree->hmtl tree conversion. - // Store the original so that conversion can revert if the ref isn't found. - return [ consumed, link ]; - } - - // [id] - // Only if id is plain (no formatting.) - if ( children.length == 1 && typeof children[0] == "string" ) { - - attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) }; - link = [ "link_ref", attrs, children[0] ]; - return [ consumed, link ]; - } - - // Just consume the "[" - return [ 1, "[" ]; - }, - - - "<": function autoLink( text ) { - var m; - - if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) { - if ( m[3] ) { - return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ]; - - } - else if ( m[2] == "mailto" ) { - return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ]; - } - else - return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ]; - } - - return [ 1, "<" ]; - }, - - "`": function inlineCode( text ) { - // Inline code block. as many backticks as you like to start it - // Always skip over the opening ticks. - var m = text.match( /(`+)(([\s\S]*?)\1)/ ); - - if ( m && m[2] ) - return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ]; - else { - // TODO: No matching end code found - warn! - return [ 1, "`" ]; - } - }, - - " \n": function lineBreak( text ) { - return [ 3, [ "linebreak" ] ]; + var html = convert_tree_to_html(input, refs, options); + merge_text_nodes(html); + return html; + }; + + // For Spidermonkey based engines + function mk_block_toSource() { + return "Markdown.mk_block( " + uneval(this.toString()) + ", " + uneval(this.trailing) + ", " + uneval(this.lineNumber) + " )"; } -}; - -// Meta Helper/generator method for em and strong handling -function strong_em( tag, md ) { - - var state_slot = tag + "_state", - other_slot = tag == "strong" ? "em_state" : "strong_state"; - - function CloseTag(len) { - this.len_after = len; - this.name = "close_" + md; - } - - return function ( text, orig_match ) { - - if ( this[state_slot][0] == md ) { - // Most recent em is of this type - //D:this.debug("closing", md); - this[state_slot].shift(); - - // "Consume" everything to go back to the recrusion in the else-block below - return[ text.length, new CloseTag(text.length-md.length) ]; + // node + function mk_block_inspect() { + var util = require("util"); + return "Markdown.mk_block( " + util.inspect(this.toString()) + ", " + util.inspect(this.trailing) + ", " + util.inspect(this.lineNumber) + " )"; } - else { - // Store a clone of the em/strong states - var other = this[other_slot].slice(), - state = this[state_slot].slice(); + var mk_block = Markdown.mk_block = function(block, trail, line) { + // Be helpful for default case in tests. + if (arguments.length == 1) trail = "\n\n"; + var s = new String(block); + s.trailing = trail; + // To make it clear its not just a string + s.inspect = mk_block_inspect; + s.toSource = mk_block_toSource; + if (line != undefined) s.lineNumber = line; + return s; + }; - this[state_slot].unshift(md); - - //D:this.debug_indent += " "; - - // Recurse - var res = this.processInline( text.substr( md.length ) ); - //D:this.debug_indent = this.debug_indent.substr(2); - - var last = res[res.length - 1]; - - //D:this.debug("processInline from", tag + ": ", uneval( res ) ); - - var check = this[state_slot].shift(); - if ( last instanceof CloseTag ) { - res.pop(); - // We matched! Huzzah. - var consumed = text.length - last.len_after; - return [ consumed, [ tag ].concat(res) ]; - } - else { - // Restore the state of the other kind. We might have mistakenly closed it. - this[other_slot] = other; - this[state_slot] = state; - - // We can't reuse the processed result as it could have wrong parsing contexts in it. - return [ md.length, md ]; - } - } - }; // End returned function -} - -Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**"); -Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__"); -Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*"); -Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_"); - - -// Build default order from insertion order. -Markdown.buildBlockOrder = function(d) { - var ord = []; - for ( var i in d ) { - if ( i == "__order__" || i == "__call__" ) continue; - ord.push( i ); - } - d.__order__ = ord; -}; - -// Build patterns for inline matcher -Markdown.buildInlinePatterns = function(d) { - var patterns = []; - - for ( var i in d ) { - // __foo__ is reserved and not a pattern - if ( i.match( /^__.*__$/) ) continue; - var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) - .replace( /\n/, "\\n" ); - patterns.push( i.length == 1 ? l : "(?:" + l + ")" ); - } - - patterns = patterns.join("|"); - d.__patterns__ = patterns; - //print("patterns:", uneval( patterns ) ); - - var fn = d.__call__; - d.__call__ = function(text, pattern) { - if ( pattern != undefined ) { - return fn.call(this, text, pattern); - } - else - { - return fn.call(this, text, patterns); - } - }; -}; - -Markdown.DialectHelpers = {}; -Markdown.DialectHelpers.inline_until_char = function( text, want ) { - var consumed = 0, - nodes = []; - - while ( true ) { - if ( text.charAt( consumed ) == want ) { - // Found the character we were looking for - consumed++; - return [ consumed, nodes ]; + function count_lines(str) { + var n = 0, + i = -1; + while ((i = str.indexOf("\n", i + 1)) !== -1) n++; + return n; } - if ( consumed >= text.length ) { - // No closing char found. Abort. - return null; - } - - var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) ); - consumed += res[ 0 ]; - // Add any returned nodes. - nodes.push.apply( nodes, res.slice( 1 ) ); - } -} - -// Helper function to make sub-classing a dialect easier -Markdown.subclassDialect = function( d ) { - function Block() {} - Block.prototype = d.block; - function Inline() {} - Inline.prototype = d.inline; - - return { block: new Block(), inline: new Inline() }; -}; - -Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block ); -Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline ); - -Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber ); - -Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) { - var meta = split_meta_hash( meta_string ), - attr = {}; - - for ( var i = 0; i < meta.length; ++i ) { - // id: #foo - if ( /^#/.test( meta[ i ] ) ) { - attr.id = meta[ i ].substring( 1 ); - } - // class: .foo - else if ( /^\./.test( meta[ i ] ) ) { - // if class already exists, append the new one - if ( attr["class"] ) { - attr["class"] = attr["class"] + meta[ i ].replace( /./, " " ); - } - else { - attr["class"] = meta[ i ].substring( 1 ); - } - } - // attribute: foo=bar - else if ( /\=/.test( meta[ i ] ) ) { - var s = meta[ i ].split( /\=/ ); - attr[ s[ 0 ] ] = s[ 1 ]; - } - } - - return attr; -} - -function split_meta_hash( meta_string ) { - var meta = meta_string.split( "" ), - parts = [ "" ], - in_quotes = false; - - while ( meta.length ) { - var letter = meta.shift(); - switch ( letter ) { - case " " : - // if we're in a quoted section, keep it - if ( in_quotes ) { - parts[ parts.length - 1 ] += letter; - } - // otherwise make a new part - else { - parts.push( "" ); - } - break; - case "'" : - case '"' : - // reverse the quotes and move straight on - in_quotes = !in_quotes; - break; - case "\\" : - // shift off the next letter to be used straight away. - // it was escaped so we'll keep it whatever it is - letter = meta.shift(); - default : - parts[ parts.length - 1 ] += letter; - break; - } - } - - return parts; -} - -Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) { - // we're only interested in the first block - if ( block.lineNumber > 1 ) return undefined; - - // document_meta blocks consist of one or more lines of `Key: Value\n` - if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined; - - // make an attribute node if it doesn't exist - if ( !extract_attr( this.tree ) ) { - this.tree.splice( 1, 0, {} ); - } - - var pairs = block.split( /\n/ ); - for ( p in pairs ) { - var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ), - key = m[ 1 ].toLowerCase(), - value = m[ 2 ]; - - this.tree[ 1 ][ key ] = value; - } - - // document_meta produces no content! - return []; -}; - -Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) { - // check if the last line of the block is an meta hash - var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ ); - if ( !m ) return undefined; - - // process the meta hash - var attr = this.dialect.processMetaHash( m[ 2 ] ); - - var hash; - - // if we matched ^ then we need to apply meta to the previous block - if ( m[ 1 ] === "" ) { - var node = this.tree[ this.tree.length - 1 ]; - hash = extract_attr( node ); - - // if the node is a string (rather than JsonML), bail - if ( typeof node === "string" ) return undefined; - - // create the attribute hash if it doesn't exist - if ( !hash ) { - hash = {}; - node.splice( 1, 0, hash ); - } - - // add the attributes in - for ( a in attr ) { - hash[ a ] = attr[ a ]; - } - - // return nothing so the meta hash is removed - return []; - } - - // pull the meta hash off the block and process what's left - var b = block.replace( /\n.*$/, "" ), - result = this.processBlock( b, [] ); - - // get or make the attributes hash - hash = extract_attr( result[ 0 ] ); - if ( !hash ) { - hash = {}; - result[ 0 ].splice( 1, 0, hash ); - } - - // attach the attributes to the block - for ( a in attr ) { - hash[ a ] = attr[ a ]; - } - - return result; -}; - -Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) { - // one or more terms followed by one or more definitions, in a single block - var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/, - list = [ "dl" ], - i, m; - - // see if we're dealing with a tight or loose block - if ( ( m = block.match( tight ) ) ) { - // pull subsequent tight DL blocks out of `next` - var blocks = [ block ]; - while ( next.length && tight.exec( next[ 0 ] ) ) { - blocks.push( next.shift() ); - } - - for ( var b = 0; b < blocks.length; ++b ) { - var m = blocks[ b ].match( tight ), - terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ), - defns = m[ 2 ].split( /\n:\s+/ ); - - // print( uneval( m ) ); - - for ( i = 0; i < terms.length; ++i ) { - list.push( [ "dt", terms[ i ] ] ); - } - - for ( i = 0; i < defns.length; ++i ) { - // run inline processing over the definition - list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) ); - } - } - } - else { - return undefined; - } - - return [ list ]; -}; - -// splits on unescaped instances of @ch. If @ch is not a character the result -// can be unpredictable - -Markdown.dialects.Maruku.block.table = function table (block, next) { - - var _split_on_unescaped = function(s, ch) { - ch = ch || '\\s'; - if (ch.match(/^[\\|\[\]{}?*.+^$]$/)) { ch = '\\' + ch; } - var res = [ ], - r = new RegExp('^((?:\\\\.|[^\\\\' + ch + '])*)' + ch + '(.*)'), + // Internal - split source into rough blocks + Markdown.prototype.split_blocks = function splitBlocks(input, startLine) { + input = input.replace(/(\r\n|\n|\r)/g, "\n"); + // [\s\S] matches _anything_ (newline or space) + // [^] is equivalent but doesn't work in IEs. + var re = /([\s\S]+?)($|\n#|\n(?:\s*\n|$)+)/g, + blocks = [], m; - while(m = s.match(r)) { - res.push(m[1]); - s = m[2]; + var line_no = 1; + if ((m = /^(\s*\n)/.exec(input)) != null) { + // skip (but count) leading blank lines + line_no += count_lines(m[0]); + re.lastIndex = m[0].length; } - res.push(s); - return res; - } - - var leading_pipe = /^ {0,3}\|(.+)\n {0,3}\|\s*([\-:]+[\-| :]*)\n((?:\s*\|.*(?:\n|$))*)(?=\n|$)/, - // find at least an unescaped pipe in each line - no_leading_pipe = /^ {0,3}(\S(?:\\.|[^\\|])*\|.*)\n {0,3}([\-:]+\s*\|[\-| :]*)\n((?:(?:\\.|[^\\|])*\|.*(?:\n|$))*)(?=\n|$)/, - i, m; - if (m = block.match(leading_pipe)) { - // remove leading pipes in contents - // (header and horizontal rule already have the leading pipe left out) - m[3] = m[3].replace(/^\s*\|/gm, ''); - } else if (! ( m = block.match(no_leading_pipe))) { - return undefined; - } - - var table = [ "table", [ "thead", [ "tr" ] ], [ "tbody" ] ]; - - // remove trailing pipes, then split on pipes - // (no escaped pipes are allowed in horizontal rule) - m[2] = m[2].replace(/\|\s*$/, '').split('|'); - - // process alignment - var html_attrs = [ ]; - forEach (m[2], function (s) { - if (s.match(/^\s*-+:\s*$/)) html_attrs.push({align: "right"}); - else if (s.match(/^\s*:-+\s*$/)) html_attrs.push({align: "left"}); - else if (s.match(/^\s*:-+:\s*$/)) html_attrs.push({align: "center"}); - else html_attrs.push({}); - }); - - // now for the header, avoid escaped pipes - m[1] = _split_on_unescaped(m[1].replace(/\|\s*$/, ''), '|'); - for (i = 0; i < m[1].length; i++) { - table[1][1].push(['th', html_attrs[i] || {}].concat( - this.processInline(m[1][i].trim()))); - } - - // now for body contents - forEach (m[3].replace(/\|\s*$/mg, '').split('\n'), function (row) { - var html_row = ['tr']; - row = _split_on_unescaped(row, '|'); - for (i = 0; i < row.length; i++) { - html_row.push(['td', html_attrs[i] || {}].concat(this.processInline(row[i].trim()))); + while ((m = re.exec(input)) !== null) { + if (m[2] == "\n#") { + m[2] = "\n"; + re.lastIndex--; + } + blocks.push(mk_block(m[1], m[2], line_no)); + line_no += count_lines(m[0]); } - table[2].push(html_row); - }, this); + return blocks; + }; - return [table]; -} - -Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) { - if ( !out.length ) { - return [ 2, "{:" ]; - } - - // get the preceeding element - var before = out[ out.length - 1 ]; - - if ( typeof before === "string" ) { - return [ 2, "{:" ]; - } - - // match a meta hash - var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ ); - - // no match, false alarm - if ( !m ) { - return [ 2, "{:" ]; - } - - // attach the attributes to the preceeding element - var meta = this.dialect.processMetaHash( m[ 1 ] ), - attr = extract_attr( before ); - - if ( !attr ) { - attr = {}; - before.splice( 1, 0, attr ); - } - - for ( var k in meta ) { - attr[ k ] = meta[ k ]; - } - - // cut out the string and replace it with nothing - return [ m[ 0 ].length, "" ]; -}; - -Markdown.dialects.Maruku.inline.__escape__ = /^\\[\\`\*_{}\[\]()#\+.!\-|:]/; - -Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block ); -Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline ); - -var isArray = Array.isArray || function(obj) { - return Object.prototype.toString.call(obj) == "[object Array]"; -}; - -var forEach; -// Don't mess with Array.prototype. Its not friendly -if ( Array.prototype.forEach ) { - forEach = function( arr, cb, thisp ) { - return arr.forEach( cb, thisp ); - }; -} -else { - forEach = function(arr, cb, thisp) { - for (var i = 0; i < arr.length; i++) { - cb.call(thisp || arr, arr[i], i, arr); - } - } -} - -var isEmpty = function( obj ) { - for ( var key in obj ) { - if ( hasOwnProperty.call( obj, key ) ) { - return false; - } - } - - return true; -} - -function extract_attr( jsonml ) { - return isArray(jsonml) - && jsonml.length > 1 - && typeof jsonml[ 1 ] === "object" - && !( isArray(jsonml[ 1 ]) ) - ? jsonml[ 1 ] - : undefined; -} - - - -/** - * renderJsonML( jsonml[, options] ) -> String - * - jsonml (Array): JsonML array to render to XML - * - options (Object): options - * - * Converts the given JsonML into well-formed XML. - * - * The options currently understood are: - * - * - root (Boolean): wether or not the root node should be included in the - * output, or just its children. The default `false` is to not include the - * root itself. - */ -expose.renderJsonML = function( jsonml, options ) { - options = options || {}; - // include the root element in the rendered output? - options.root = options.root || false; - - var content = []; - - if ( options.root ) { - content.push( render_tree( jsonml ) ); - } - else { - jsonml.shift(); // get rid of the tag - if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { - jsonml.shift(); // get rid of the attributes - } - - while ( jsonml.length ) { - content.push( render_tree( jsonml.shift() ) ); - } - } - - return content.join( "\n\n" ); -}; - -function escapeHTML( text ) { - return text.replace( /&/g, "&" ) - .replace( //g, ">" ) - .replace( /"/g, """ ) - .replace( /'/g, "'" ); -} - -function render_tree( jsonml ) { - // basic case - if ( typeof jsonml === "string" ) { - return escapeHTML( jsonml ); - } - - var tag = jsonml.shift(), - attributes = {}, - content = []; - - if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { - attributes = jsonml.shift(); - } - - while ( jsonml.length ) { - content.push( render_tree( jsonml.shift() ) ); - } - - var tag_attrs = ""; - for ( var a in attributes ) { - tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"'; - } - - // be careful about adding whitespace here for inline elements - if ( tag == "img" || tag == "br" || tag == "hr" ) { - return "<"+ tag + tag_attrs + "/>"; - } - else { - return "<"+ tag + tag_attrs + ">" + content.join( "" ) + ""; - } -} - -function convert_tree_to_html( tree, references, options ) { - var i; - options = options || {}; - - // shallow clone - var jsonml = tree.slice( 0 ); - - if ( typeof options.preprocessTreeNode === "function" ) { - jsonml = options.preprocessTreeNode(jsonml, references); - } - - // Clone attributes if they exist - var attrs = extract_attr( jsonml ); - if ( attrs ) { - jsonml[ 1 ] = {}; - for ( i in attrs ) { - jsonml[ 1 ][ i ] = attrs[ i ]; - } - attrs = jsonml[ 1 ]; - } - - // basic case - if ( typeof jsonml === "string" ) { - return jsonml; - } - - // convert this node - switch ( jsonml[ 0 ] ) { - case "header": - jsonml[ 0 ] = "h" + jsonml[ 1 ].level; - delete jsonml[ 1 ].level; - break; - case "bulletlist": - jsonml[ 0 ] = "ul"; - break; - case "numberlist": - jsonml[ 0 ] = "ol"; - break; - case "listitem": - jsonml[ 0 ] = "li"; - break; - case "para": - jsonml[ 0 ] = "p"; - break; - case "markdown": - jsonml[ 0 ] = "html"; - if ( attrs ) delete attrs.references; - break; - case "code_block": - jsonml[ 0 ] = "pre"; - i = attrs ? 2 : 1; - var code = [ "code" ]; - code.push.apply( code, jsonml.splice( i, jsonml.length - i ) ); - jsonml[ i ] = code; - break; - case "inlinecode": - jsonml[ 0 ] = "code"; - break; - case "img": - jsonml[ 1 ].src = jsonml[ 1 ].href; - delete jsonml[ 1 ].href; - break; - case "linebreak": - jsonml[ 0 ] = "br"; - break; - case "link": - jsonml[ 0 ] = "a"; - break; - case "link_ref": - jsonml[ 0 ] = "a"; - - // grab this ref and clean up the attribute node - var ref = references[ attrs.ref ]; - - // if the reference exists, make the link - if ( ref ) { - delete attrs.ref; - - // add in the href and title, if present - attrs.href = ref.href; - if ( ref.title ) { - attrs.title = ref.title; + /** + * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] + * - block (String): the block to process + * - next (Array): the following blocks + * + * Process `block` and return an array of JsonML nodes representing `block`. + * + * It does this by asking each block level function in the dialect to process + * the block until one can. Succesful handling is indicated by returning an + * array (with zero or more JsonML nodes), failure by a false value. + * + * Blocks handlers are responsible for calling [[Markdown#processInline]] + * themselves as appropriate. + * + * If the blocks were split incorrectly or adjacent blocks need collapsing you + * can adjust `next` in place using shift/splice etc. + * + * If any of this default behaviour is not right for the dialect, you can + * define a `__call__` method on the dialect that will get invoked to handle + * the block processing. + */ + function processBlock(block, next) { + var cbs = this.dialect.block, + ord = cbs.__order__; + if ("__call__" in cbs) { + return cbs.__call__.call(this, block, next); + } + for (var i = 0; i < ord.length; i++) { + //D:this.debug( "Testing", ord[i] ); + var res = cbs[ord[i]].call(this, block, next); + if (res) { + //D:this.debug(" matched"); + if (!isArray(res) || res.length > 0 && !isArray(res[0])) this.debug(ord[i], "didn't return a proper array"); + //D:this.debug( "" ); + return res; + } } - // get rid of the unneeded original text - delete attrs.original; - } - // the reference doesn't exist, so revert to plain text - else { - return attrs.original; - } - break; - case "img_ref": - jsonml[ 0 ] = "img"; + // Uhoh! no match! Should we throw an error? + return []; + }; + Markdown.prototype.processBlock = processBlock; - // grab this ref and clean up the attribute node - var ref = references[ attrs.ref ]; + function processInline(block) { + return this.dialect.inline.__call__.call(this, String(block)); + }; + Markdown.prototype.processInline = processInline; - // if the reference exists, make the link - if ( ref ) { - delete attrs.ref; + /** + * Markdown#toTree( source ) -> JsonML + * - source (String): markdown source to parse + * + * Parse `source` into a JsonML tree representing the markdown document. + **/ + // custom_tree means set this.tree to `custom_tree` and restore old value on return + Markdown.prototype.toTree = function toTree(source, custom_root) { + var blocks = source instanceof Array ? source : this.split_blocks(source); - // add in the href and title, if present - attrs.src = ref.href; - if ( ref.title ) { - attrs.title = ref.title; + // Make tree a member variable so its easier to mess with in extensions + var old_tree = this.tree; + try { + this.tree = custom_root || this.tree || ["markdown"]; + blocks: while (blocks.length) { + var b = this.processBlock(blocks.shift(), blocks); + + // Reference blocks and the like won't return any content + if (!b.length) continue blocks; + this.tree.push.apply(this.tree, b); + } + return this.tree; + } finally { + if (custom_root) { + this.tree = old_tree; + } + } + }; + + // Noop by default + Markdown.prototype.debug = function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(this.debug_indent); + if (typeof print !== "undefined") print.apply(print, args); + if (typeof console !== "undefined" && typeof console.log !== "undefined") console.log.apply(null, args); + }; + Markdown.prototype.loop_re_over_block = function(re, block, cb) { + // Dont use /g regexps with this + var m, + b = block.valueOf(); + while (b.length && (m = re.exec(b)) != null) { + b = b.substr(m[0].length); + cb.call(this, m); + } + return b; + }; + + /** + * Markdown.dialects + * + * Namespace of built-in dialects. + **/ + Markdown.dialects = {}; + + /** + * Markdown.dialects.Gruber + * + * The default dialect that follows the rules set out by John Gruber's + * markdown.pl as closely as possible. Well actually we follow the behaviour of + * that script which in some places is not exactly what the syntax web page + * says. + **/ + Markdown.dialects.Gruber = { + block: { + atxHeader: function atxHeader(block, next) { + var m = block.match(/^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/); + if (!m) return undefined; + var header = ["header", { + level: m[1].length + }]; + Array.prototype.push.apply(header, this.processInline(m[2])); + if (m[0].length < block.length) next.unshift(mk_block(block.substr(m[0].length), block.trailing, block.lineNumber + 2)); + return [header]; + }, + setextHeader: function setextHeader(block, next) { + var m = block.match(/^(.*)\n([-=])\2\2+(?:\n|$)/); + if (!m) return undefined; + var level = m[2] === "=" ? 1 : 2; + var header = ["header", { + level: level + }, m[1]]; + if (m[0].length < block.length) next.unshift(mk_block(block.substr(m[0].length), block.trailing, block.lineNumber + 2)); + return [header]; + }, + code: function code(block, next) { + // | Foo + // |bar + // should be a code block followed by a paragraph. Fun + // + // There might also be adjacent code block to merge. + + var ret = [], + re = /^(?: {0,3}\t| {4})(.*)\n?/, + lines; + + // 4 spaces + content + if (!block.match(re)) return undefined; + block_search: do { + // Now pull out the rest of the lines + var b = this.loop_re_over_block(re, block.valueOf(), function(m) { + ret.push(m[1]); + }); + if (b.length) { + // Case alluded to in first comment. push it back on as a new block + next.unshift(mk_block(b, block.trailing)); + break block_search; + } else if (next.length) { + // Check the next block - it might be code too + if (!next[0].match(re)) break block_search; + + // Pull how how many blanks lines follow - minus two to account for .join + ret.push(block.trailing.replace(/[^\n]/g, "").substring(2)); + block = next.shift(); + } else { + break block_search; + } + } while (true); + return [ + ["code_block", ret.join("\n")] + ]; + }, + horizRule: function horizRule(block, next) { + // this needs to find any hr in the block to handle abutting blocks + var m = block.match(/^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/); + if (!m) { + return undefined; + } + var jsonml = [ + ["hr"] + ]; + + // if there's a leading abutting block, process it + if (m[1]) { + jsonml.unshift.apply(jsonml, this.processBlock(m[1], [])); + } + + // if there's a trailing abutting block, stick it into next + if (m[3]) { + next.unshift(mk_block(m[3])); + } + return jsonml; + }, + // There are two types of lists. Tight and loose. Tight lists have no whitespace + // between the items (and result in text just in the
  • ) and loose lists, + // which have an empty line between list items, resulting in (one or more) + // paragraphs inside the
  • . + // + // There are all sorts weird edge cases about the original markdown.pl's + // handling of lists: + // + // * Nested lists are supposed to be indented by four chars per level. But + // if they aren't, you can get a nested list by indenting by less than + // four so long as the indent doesn't match an indent of an existing list + // item in the 'nest stack'. + // + // * The type of the list (bullet or number) is controlled just by the + // first item at the indent. Subsequent changes are ignored unless they + // are for nested lists + // + lists: function() { + // Use a closure to hide a few variables. + var any_list = "[*+-]|\\d+\\.", + bullet_list = /[*+-]/, + number_list = /\d+\./, + // Capture leading indent as it matters for determining nested lists. + is_list_re = new RegExp("^( {0,3})(" + any_list + ")[ \t]+"), + indent_re = "(?: {0,3}\\t| {4})"; + + // TODO: Cache this regexp for certain depths. + // Create a regexp suitable for matching an li for a given stack depth + function regex_for_depth(depth) { + return new RegExp( + // m[1] = indent, m[2] = list_type + "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" + + // m[3] = cont + "(^" + indent_re + "{0," + (depth - 1) + "}[ ]{0,4})"); + } + + function expand_tab(input) { + return input.replace(/ {0,3}\t/g, " "); + } + + // Add inline content `inline` to `li`. inline comes from processInline + // so is an array of content + function add(li, loose, inline, nl) { + if (loose) { + li.push(["para"].concat(inline)); + return; + } + // Hmmm, should this be any block level element or just paras? + var add_to = li[li.length - 1] instanceof Array && li[li.length - 1][0] == "para" ? li[li.length - 1] : li; + + // If there is already some content in this list, add the new line in + if (nl && li.length > 1) inline.unshift(nl); + for (var i = 0; i < inline.length; i++) { + var what = inline[i], + is_str = typeof what == "string"; + if (is_str && add_to.length > 1 && typeof add_to[add_to.length - 1] == "string") { + add_to[add_to.length - 1] += what; + } else { + add_to.push(what); + } + } + } + + // contained means have an indent greater than the current one. On + // *every* line in the block + function get_contained_blocks(depth, blocks) { + var re = new RegExp("^(" + indent_re + "{" + depth + "}.*?\\n?)*$"), + replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"), + ret = []; + while (blocks.length > 0) { + if (re.exec(blocks[0])) { + var b = blocks.shift(), + // Now remove that indent + x = b.replace(replace, ""); + ret.push(mk_block(x, b.trailing, b.lineNumber)); + } else { + break; + } + } + return ret; + } + + // passed to stack.forEach to turn list items up the stack into paras + function paragraphify(s, i, stack) { + var list = s.list; + var last_li = list[list.length - 1]; + if (last_li[1] instanceof Array && last_li[1][0] == "para") { + return; + } + if (i + 1 == stack.length) { + // Last stack frame + // Keep the same array, but replace the contents + last_li.push(["para"].concat(last_li.splice(1, last_li.length - 1))); + } else { + var sublist = last_li.pop(); + last_li.push(["para"].concat(last_li.splice(1, last_li.length - 1)), sublist); + } + } + + // The matcher function + return function(block, next) { + var m = block.match(is_list_re); + if (!m) return undefined; + + function make_list(m) { + var list = bullet_list.exec(m[2]) ? ["bulletlist"] : ["numberlist"]; + stack.push({ + list: list, + indent: m[1] + }); + return list; + } + var stack = [], + // Stack of lists for nesting. + list = make_list(m), + last_li, + loose = false, + ret = [stack[0].list], + i; + + // Loop to search over block looking for inner block elements and loose lists + loose_search: while (true) { + // Split into lines preserving new lines at end of line + var lines = block.split(/(?=\n)/); + + // We have to grab all lines for a li and call processInline on them + // once as there are some inline things that can span lines. + var li_accumulate = ""; + + // Loop over the lines in this block looking for tight lists. + tight_search: for (var line_no = 0; line_no < lines.length; line_no++) { + var nl = "", + l = lines[line_no].replace(/^\n/, function(n) { + nl = n; + return ""; + }); + + // TODO: really should cache this + var line_re = regex_for_depth(stack.length); + m = l.match(line_re); + //print( "line:", uneval(l), "\nline match:", uneval(m) ); + + // We have a list item + if (m[1] !== undefined) { + // Process the previous list item, if any + if (li_accumulate.length) { + add(last_li, loose, this.processInline(li_accumulate), nl); + // Loose mode will have been dealt with. Reset it + loose = false; + li_accumulate = ""; + } + m[1] = expand_tab(m[1]); + var wanted_depth = Math.floor(m[1].length / 4) + 1; + //print( "want:", wanted_depth, "stack:", stack.length); + if (wanted_depth > stack.length) { + // Deep enough for a nested list outright + //print ( "new nested list" ); + list = make_list(m); + last_li.push(list); + last_li = list[1] = ["listitem"]; + } else { + // We aren't deep enough to be strictly a new level. This is + // where Md.pl goes nuts. If the indent matches a level in the + // stack, put it there, else put it one deeper then the + // wanted_depth deserves. + var found = false; + for (i = 0; i < stack.length; i++) { + if (stack[i].indent != m[1]) continue; + list = stack[i].list; + stack.splice(i + 1, stack.length - (i + 1)); + found = true; + break; + } + if (!found) { + //print("not found. l:", uneval(l)); + wanted_depth++; + if (wanted_depth <= stack.length) { + stack.splice(wanted_depth, stack.length - wanted_depth); + //print("Desired depth now", wanted_depth, "stack:", stack.length); + list = stack[wanted_depth - 1].list; + //print("list:", uneval(list) ); + } else { + //print ("made new stack for messy indent"); + list = make_list(m); + last_li.push(list); + } + } + + //print( uneval(list), "last", list === stack[stack.length-1].list ); + last_li = ["listitem"]; + list.push(last_li); + } // end depth of shenegains + nl = ""; + } + + // Add content + if (l.length > m[0].length) { + li_accumulate += nl + l.substr(m[0].length); + } + } // tight_search + + if (li_accumulate.length) { + add(last_li, loose, this.processInline(li_accumulate), nl); + // Loose mode will have been dealt with. Reset it + loose = false; + li_accumulate = ""; + } + + // Look at the next block - we might have a loose list. Or an extra + // paragraph for the current li + var contained = get_contained_blocks(stack.length, next); + + // Deal with code blocks or properly nested lists + if (contained.length > 0) { + // Make sure all listitems up the stack are paragraphs + forEach(stack, paragraphify, this); + last_li.push.apply(last_li, this.toTree(contained, [])); + } + var next_block = next[0] && next[0].valueOf() || ""; + if (next_block.match(is_list_re) || next_block.match(/^ /)) { + block = next.shift(); + + // Check for an HR following a list: features/lists/hr_abutting + var hr = this.dialect.block.horizRule(block, next); + if (hr) { + ret.push.apply(ret, hr); + break; + } + + // Make sure all listitems up the stack are paragraphs + forEach(stack, paragraphify, this); + loose = true; + continue loose_search; + } + break; + } // loose_search + + return ret; + }; + }(), + blockquote: function blockquote(block, next) { + if (!block.match(/^>/m)) return undefined; + var jsonml = []; + + // separate out the leading abutting block, if any. I.e. in this case: + // + // a + // > b + // + if (block[0] != ">") { + var lines = block.split(/\n/), + prev = [], + line_no = block.lineNumber; + + // keep shifting lines until you find a crotchet + while (lines.length && lines[0][0] != ">") { + prev.push(lines.shift()); + line_no++; + } + var abutting = mk_block(prev.join("\n"), "\n", block.lineNumber); + jsonml.push.apply(jsonml, this.processBlock(abutting, [])); + // reassemble new block of just block quotes! + block = mk_block(lines.join("\n"), block.trailing, line_no); + } + + // if the next block is also a blockquote merge it in + while (next.length && next[0][0] == ">") { + var b = next.shift(); + block = mk_block(block + block.trailing + b, b.trailing, block.lineNumber); + } + + // Strip off the leading "> " and re-process as a block. + var input = block.replace(/^> ?/gm, ""), + old_tree = this.tree, + processedBlock = this.toTree(input, ["blockquote"]), + attr = extract_attr(processedBlock); + + // If any link references were found get rid of them + if (attr && attr.references) { + delete attr.references; + // And then remove the attribute object if it's empty + if (isEmpty(attr)) { + processedBlock.splice(1, 1); + } + } + jsonml.push(processedBlock); + return jsonml; + }, + referenceDefn: function referenceDefn(block, next) { + var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; + // interesting matches are [ , ref_id, url, , title, title ] + + if (!block.match(re)) return undefined; + + // make an attribute node if it doesn't exist + if (!extract_attr(this.tree)) { + this.tree.splice(1, 0, {}); + } + var attrs = extract_attr(this.tree); + + // make a references hash if it doesn't exist + if (attrs.references === undefined) { + attrs.references = {}; + } + var b = this.loop_re_over_block(re, block, function(m) { + if (m[2] && m[2][0] == "<" && m[2][m[2].length - 1] == ">") m[2] = m[2].substring(1, m[2].length - 1); + var ref = attrs.references[m[1].toLowerCase()] = { + href: m[2] + }; + if (m[4] !== undefined) ref.title = m[4]; + else if (m[5] !== undefined) ref.title = m[5]; + }); + if (b.length) next.unshift(mk_block(b, block.trailing)); + return []; + }, + para: function para(block, next) { + // everything's a para! + return [ + ["para"].concat(this.processInline(block)) + ]; + } + } + }; + Markdown.dialects.Gruber.inline = { + __oneElement__: function oneElement(text, patterns_or_re, previous_nodes) { + var m, + res, + lastIndex = 0; + patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; + var re = new RegExp("([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")"); + m = re.exec(text); + if (!m) { + // Just boring text + return [text.length, text]; + } else if (m[1]) { + // Some un-interesting text matched. Return that first + return [m[1].length, m[1]]; + } + var res; + if (m[2] in this.dialect.inline) { + res = this.dialect.inline[m[2]].call(this, text.substr(m.index), m, previous_nodes || []); + } + // Default for now to make dev easier. just slurp special and output it. + res = res || [m[2].length, m[2]]; + return res; + }, + __call__: function inline(text, patterns) { + var out = [], + res; + + function add(x) { + //D:self.debug(" adding output", uneval(x)); + if (typeof x == "string" && typeof out[out.length - 1] == "string") out[out.length - 1] += x; + else out.push(x); + } + while (text.length > 0) { + res = this.dialect.inline.__oneElement__.call(this, text, patterns, out); + text = text.substr(res.shift()); + forEach(res, add); + } + return out; + }, + // These characters are intersting elsewhere, so have rules for them so that + // chunks of plain text blocks don't include them + "]": function _() {}, + "}": function _() {}, + __escape__: /^\\[\\`\*_{}\[\]()#\+.!\-]/, + "\\": function escaped(text) { + // [ length of input processed, node/children to add... ] + // Only esacape: \ ` * _ { } [ ] ( ) # * + - . ! + if (this.dialect.inline.__escape__.exec(text)) return [2, text.charAt(1)]; + else + // Not an esacpe + return [1, "\\"]; + }, + "![": function image(text) { + // Unlike images, alt text is plain text only. no other elements are + // allowed in there + + // ![Alt text](/path/to/img.jpg "Optional title") + // 1 2 3 4 <--- captures + var m = text.match(/^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/); + if (m) { + if (m[2] && m[2][0] == "<" && m[2][m[2].length - 1] == ">") m[2] = m[2].substring(1, m[2].length - 1); + m[2] = this.dialect.inline.__call__.call(this, m[2], /\\/)[0]; + var attrs = { + alt: m[1], + href: m[2] || "" + }; + if (m[4] !== undefined) attrs.title = m[4]; + return [m[0].length, ["img", attrs]]; + } + + // ![Alt text][id] + m = text.match(/^!\[(.*?)\][ \t]*\[(.*?)\]/); + if (m) { + // We can't check if the reference is known here as it likely wont be + // found till after. Check it in md tree->hmtl tree conversion + return [m[0].length, ["img_ref", { + alt: m[1], + ref: m[2].toLowerCase(), + original: m[0] + }]]; + } + + // Just consume the '![' + return [2, "!["]; + }, + "[": function link(text) { + var orig = String(text); + // Inline content is possible inside `link text` + var res = Markdown.DialectHelpers.inline_until_char.call(this, text.substr(1), "]"); + + // No closing ']' found. Just consume the [ + if (!res) return [1, "["]; + var consumed = 1 + res[0], + children = res[1], + link, + attrs; + + // At this point the first [...] has been parsed. See what follows to find + // out which kind of link we are (reference or direct url) + text = text.substr(consumed); + + // [link text](/path/to/img.jpg "Optional title") + // 1 2 3 <--- captures + // This will capture up to the last paren in the block. We then pull + // back based on if there a matching ones in the url + // ([here](/url/(test)) + // The parens have to be balanced + var m = text.match(/^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/); + if (m) { + var url = m[1]; + consumed += m[0].length; + if (url && url[0] == "<" && url[url.length - 1] == ">") url = url.substring(1, url.length - 1); + + // If there is a title we don't have to worry about parens in the url + if (!m[3]) { + var open_parens = 1; // One open that isn't in the capture + for (var len = 0; len < url.length; len++) { + switch (url[len]) { + case "(": + open_parens++; + break; + case ")": + if (--open_parens == 0) { + consumed -= url.length - len; + url = url.substring(0, len); + } + break; + } + } + } + + // Process escapes only + url = this.dialect.inline.__call__.call(this, url, /\\/)[0]; + attrs = { + href: url || "" + }; + if (m[3] !== undefined) attrs.title = m[3]; + link = ["link", attrs].concat(children); + return [consumed, link]; + } + + // [Alt text][id] + // [Alt text] [id] + m = text.match(/^\s*\[(.*?)\]/); + if (m) { + consumed += m[0].length; + + // [links][] uses links as its reference + attrs = { + ref: (m[1] || String(children)).toLowerCase(), + original: orig.substr(0, consumed) + }; + link = ["link_ref", attrs].concat(children); + + // We can't check if the reference is known here as it likely wont be + // found till after. Check it in md tree->hmtl tree conversion. + // Store the original so that conversion can revert if the ref isn't found. + return [consumed, link]; + } + + // [id] + // Only if id is plain (no formatting.) + if (children.length == 1 && typeof children[0] == "string") { + attrs = { + ref: children[0].toLowerCase(), + original: orig.substr(0, consumed) + }; + link = ["link_ref", attrs, children[0]]; + return [consumed, link]; + } + + // Just consume the "[" + return [1, "["]; + }, + "<": function autoLink(text) { + var m; + if ((m = text.match(/^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/)) != null) { + if (m[3]) { + return [m[0].length, ["link", { + href: "mailto:" + m[3] + }, m[3]]]; + } else if (m[2] == "mailto") { + return [m[0].length, ["link", { + href: m[1] + }, m[1].substr("mailto:".length)]]; + } else return [m[0].length, ["link", { + href: m[1] + }, m[1]]]; + } + return [1, "<"]; + }, + "`": function inlineCode(text) { + // Inline code block. as many backticks as you like to start it + // Always skip over the opening ticks. + var m = text.match(/(`+)(([\s\S]*?)\1)/); + if (m && m[2]) return [m[1].length + m[2].length, ["inlinecode", m[3]]]; + else { + // TODO: No matching end code found - warn! + return [1, "`"]; + } + }, + " \n": function lineBreak(text) { + return [3, ["linebreak"]]; + } + }; + + // Meta Helper/generator method for em and strong handling + function strong_em(tag, md) { + var state_slot = tag + "_state", + other_slot = tag == "strong" ? "em_state" : "strong_state"; + + function CloseTag(len) { + this.len_after = len; + this.name = "close_" + md; + } + return function(text, orig_match) { + if (this[state_slot][0] == md) { + // Most recent em is of this type + //D:this.debug("closing", md); + this[state_slot].shift(); + + // "Consume" everything to go back to the recrusion in the else-block below + return [text.length, new CloseTag(text.length - md.length)]; + } else { + // Store a clone of the em/strong states + var other = this[other_slot].slice(), + state = this[state_slot].slice(); + this[state_slot].unshift(md); + + //D:this.debug_indent += " "; + + // Recurse + var res = this.processInline(text.substr(md.length)); + //D:this.debug_indent = this.debug_indent.substr(2); + + var last = res[res.length - 1]; + + //D:this.debug("processInline from", tag + ": ", uneval( res ) ); + + var check = this[state_slot].shift(); + if (last instanceof CloseTag) { + res.pop(); + // We matched! Huzzah. + var consumed = text.length - last.len_after; + return [consumed, [tag].concat(res)]; + } else { + // Restore the state of the other kind. We might have mistakenly closed it. + this[other_slot] = other; + this[state_slot] = state; + + // We can't reuse the processed result as it could have wrong parsing contexts in it. + return [md.length, md]; + } + } + }; // End returned function + } + Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**"); + Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__"); + Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*"); + Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_"); + + // Build default order from insertion order. + Markdown.buildBlockOrder = function(d) { + var ord = []; + for (var i in d) { + if (i == "__order__" || i == "__call__") continue; + ord.push(i); + } + d.__order__ = ord; + }; + + // Build patterns for inline matcher + Markdown.buildInlinePatterns = function(d) { + var patterns = []; + for (var i in d) { + // __foo__ is reserved and not a pattern + if (i.match(/^__.*__$/)) continue; + var l = i.replace(/([\\.*+?|()\[\]{}])/g, "\\$1").replace(/\n/, "\\n"); + patterns.push(i.length == 1 ? l : "(?:" + l + ")"); + } + patterns = patterns.join("|"); + d.__patterns__ = patterns; + //print("patterns:", uneval( patterns ) ); + + var fn = d.__call__; + d.__call__ = function(text, pattern) { + if (pattern != undefined) { + return fn.call(this, text, pattern); + } else { + return fn.call(this, text, patterns); + } + }; + }; + Markdown.DialectHelpers = {}; + Markdown.DialectHelpers.inline_until_char = function(text, want) { + var consumed = 0, + nodes = []; + while (true) { + if (text.charAt(consumed) == want) { + // Found the character we were looking for + consumed++; + return [consumed, nodes]; + } + if (consumed >= text.length) { + // No closing char found. Abort. + return null; + } + var res = this.dialect.inline.__oneElement__.call(this, text.substr(consumed)); + consumed += res[0]; + // Add any returned nodes. + nodes.push.apply(nodes, res.slice(1)); + } + }; + + // Helper function to make sub-classing a dialect easier + Markdown.subclassDialect = function(d) { + function Block() {} + Block.prototype = d.block; + + function Inline() {} + Inline.prototype = d.inline; + return { + block: new Block(), + inline: new Inline() + }; + }; + Markdown.buildBlockOrder(Markdown.dialects.Gruber.block); + Markdown.buildInlinePatterns(Markdown.dialects.Gruber.inline); + Markdown.dialects.Maruku = Markdown.subclassDialect(Markdown.dialects.Gruber); + Markdown.dialects.Maruku.processMetaHash = function processMetaHash(meta_string) { + var meta = split_meta_hash(meta_string), + attr = {}; + for (var i = 0; i < meta.length; ++i) { + // id: #foo + if (/^#/.test(meta[i])) { + attr.id = meta[i].substring(1); + } + // class: .foo + else if (/^\./.test(meta[i])) { + // if class already exists, append the new one + if (attr["class"]) { + attr["class"] = attr["class"] + meta[i].replace(/./, " "); + } else { + attr["class"] = meta[i].substring(1); + } + } + // attribute: foo=bar + else if (/\=/.test(meta[i])) { + var s = meta[i].split(/\=/); + attr[s[0]] = s[1]; + } + } + return attr; + }; + + function split_meta_hash(meta_string) { + var meta = meta_string.split(""), + parts = [""], + in_quotes = false; + while (meta.length) { + var letter = meta.shift(); + switch (letter) { + case " ": + // if we're in a quoted section, keep it + if (in_quotes) { + parts[parts.length - 1] += letter; + } + // otherwise make a new part + else { + parts.push(""); + } + break; + case "'": + case '"': + // reverse the quotes and move straight on + in_quotes = !in_quotes; + break; + case "\\": + // shift off the next letter to be used straight away. + // it was escaped so we'll keep it whatever it is + letter = meta.shift(); + default: + parts[parts.length - 1] += letter; + break; + } + } + return parts; + } + Markdown.dialects.Maruku.block.document_meta = function document_meta(block, next) { + // we're only interested in the first block + if (block.lineNumber > 1) return undefined; + + // document_meta blocks consist of one or more lines of `Key: Value\n` + if (!block.match(/^(?:\w+:.*\n)*\w+:.*$/)) return undefined; + + // make an attribute node if it doesn't exist + if (!extract_attr(this.tree)) { + this.tree.splice(1, 0, {}); + } + var pairs = block.split(/\n/); + for (p in pairs) { + var m = pairs[p].match(/(\w+):\s*(.*)$/), + key = m[1].toLowerCase(), + value = m[2]; + this.tree[1][key] = value; } - // get rid of the unneeded original text - delete attrs.original; - } - // the reference doesn't exist, so revert to plain text - else { - return attrs.original; - } - break; - } + // document_meta produces no content! + return []; + }; + Markdown.dialects.Maruku.block.block_meta = function block_meta(block, next) { + // check if the last line of the block is an meta hash + var m = block.match(/(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/); + if (!m) return undefined; - // convert all the children - i = 1; + // process the meta hash + var attr = this.dialect.processMetaHash(m[2]); + var hash; - // deal with the attribute node, if it exists - if ( attrs ) { - // if there are keys, skip over it - for ( var key in jsonml[ 1 ] ) { - i = 2; - break; + // if we matched ^ then we need to apply meta to the previous block + if (m[1] === "") { + var node = this.tree[this.tree.length - 1]; + hash = extract_attr(node); + + // if the node is a string (rather than JsonML), bail + if (typeof node === "string") return undefined; + + // create the attribute hash if it doesn't exist + if (!hash) { + hash = {}; + node.splice(1, 0, hash); + } + + // add the attributes in + for (a in attr) { + hash[a] = attr[a]; + } + + // return nothing so the meta hash is removed + return []; + } + + // pull the meta hash off the block and process what's left + var b = block.replace(/\n.*$/, ""), + result = this.processBlock(b, []); + + // get or make the attributes hash + hash = extract_attr(result[0]); + if (!hash) { + hash = {}; + result[0].splice(1, 0, hash); + } + + // attach the attributes to the block + for (a in attr) { + hash[a] = attr[a]; + } + return result; + }; + Markdown.dialects.Maruku.block.definition_list = function definition_list(block, next) { + // one or more terms followed by one or more definitions, in a single block + var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/, + list = ["dl"], + i, + m; + + // see if we're dealing with a tight or loose block + if (m = block.match(tight)) { + // pull subsequent tight DL blocks out of `next` + var blocks = [block]; + while (next.length && tight.exec(next[0])) { + blocks.push(next.shift()); + } + for (var b = 0; b < blocks.length; ++b) { + var m = blocks[b].match(tight), + terms = m[1].replace(/\n$/, "").split(/\n/), + defns = m[2].split(/\n:\s+/); + + // print( uneval( m ) ); + + for (i = 0; i < terms.length; ++i) { + list.push(["dt", terms[i]]); + } + for (i = 0; i < defns.length; ++i) { + // run inline processing over the definition + list.push(["dd"].concat(this.processInline(defns[i].replace(/(\n)\s+/, "$1")))); + } + } + } else { + return undefined; + } + return [list]; + }; + + // splits on unescaped instances of @ch. If @ch is not a character the result + // can be unpredictable + + Markdown.dialects.Maruku.block.table = function table(block, next) { + var _split_on_unescaped = function _split_on_unescaped(s, ch) { + ch = ch || '\\s'; + if (ch.match(/^[\\|\[\]{}?*.+^$]$/)) { + ch = '\\' + ch; + } + var res = [], + r = new RegExp('^((?:\\\\.|[^\\\\' + ch + '])*)' + ch + '(.*)'), + m; + while (m = s.match(r)) { + res.push(m[1]); + s = m[2]; + } + res.push(s); + return res; + }; + var leading_pipe = /^ {0,3}\|(.+)\n {0,3}\|\s*([\-:]+[\-| :]*)\n((?:\s*\|.*(?:\n|$))*)(?=\n|$)/, + // find at least an unescaped pipe in each line + no_leading_pipe = /^ {0,3}(\S(?:\\.|[^\\|])*\|.*)\n {0,3}([\-:]+\s*\|[\-| :]*)\n((?:(?:\\.|[^\\|])*\|.*(?:\n|$))*)(?=\n|$)/, + i, + m; + if (m = block.match(leading_pipe)) { + // remove leading pipes in contents + // (header and horizontal rule already have the leading pipe left out) + m[3] = m[3].replace(/^\s*\|/gm, ''); + } else if (!(m = block.match(no_leading_pipe))) { + return undefined; + } + var table = ["table", ["thead", ["tr"]], + ["tbody"] + ]; + + // remove trailing pipes, then split on pipes + // (no escaped pipes are allowed in horizontal rule) + m[2] = m[2].replace(/\|\s*$/, '').split('|'); + + // process alignment + var html_attrs = []; + forEach(m[2], function(s) { + if (s.match(/^\s*-+:\s*$/)) html_attrs.push({ + align: "right" + }); + else if (s.match(/^\s*:-+\s*$/)) html_attrs.push({ + align: "left" + }); + else if (s.match(/^\s*:-+:\s*$/)) html_attrs.push({ + align: "center" + }); + else html_attrs.push({}); + }); + + // now for the header, avoid escaped pipes + m[1] = _split_on_unescaped(m[1].replace(/\|\s*$/, ''), '|'); + for (i = 0; i < m[1].length; i++) { + table[1][1].push(['th', html_attrs[i] || {}].concat(this.processInline(m[1][i].trim()))); + } + + // now for body contents + forEach(m[3].replace(/\|\s*$/mg, '').split('\n'), function(row) { + var html_row = ['tr']; + row = _split_on_unescaped(row, '|'); + for (i = 0; i < row.length; i++) { + html_row.push(['td', html_attrs[i] || {}].concat(this.processInline(row[i].trim()))); + } + table[2].push(html_row); + }, this); + return [table]; + }; + Markdown.dialects.Maruku.inline["{:"] = function inline_meta(text, matches, out) { + if (!out.length) { + return [2, "{:"]; + } + + // get the preceeding element + var before = out[out.length - 1]; + if (typeof before === "string") { + return [2, "{:"]; + } + + // match a meta hash + var m = text.match(/^\{:\s*((?:\\\}|[^\}])*)\s*\}/); + + // no match, false alarm + if (!m) { + return [2, "{:"]; + } + + // attach the attributes to the preceeding element + var meta = this.dialect.processMetaHash(m[1]), + attr = extract_attr(before); + if (!attr) { + attr = {}; + before.splice(1, 0, attr); + } + for (var k in meta) { + attr[k] = meta[k]; + } + + // cut out the string and replace it with nothing + return [m[0].length, ""]; + }; + Markdown.dialects.Maruku.inline.__escape__ = /^\\[\\`\*_{}\[\]()#\+.!\-|:]/; + Markdown.buildBlockOrder(Markdown.dialects.Maruku.block); + Markdown.buildInlinePatterns(Markdown.dialects.Maruku.inline); + var isArray = Array.isArray || function(obj) { + return Object.prototype.toString.call(obj) == "[object Array]"; + }; + var forEach; + // Don't mess with Array.prototype. Its not friendly + if (Array.prototype.forEach) { + forEach = function forEach(arr, cb, thisp) { + return arr.forEach(cb, thisp); + }; + } else { + forEach = function forEach(arr, cb, thisp) { + for (var i = 0; i < arr.length; i++) { + cb.call(thisp || arr, arr[i], i, arr); + } + }; } - // if there aren't, remove it - if ( i === 1 ) { - jsonml.splice( i, 1 ); + var isEmpty = function isEmpty(obj) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + }; + + function extract_attr(jsonml) { + return isArray(jsonml) && jsonml.length > 1 && _typeof(jsonml[1]) === "object" && !isArray(jsonml[1]) ? jsonml[1] : undefined; } - } - for ( ; i < jsonml.length; ++i ) { - jsonml[ i ] = convert_tree_to_html( jsonml[ i ], references, options ); - } + /** + * renderJsonML( jsonml[, options] ) -> String + * - jsonml (Array): JsonML array to render to XML + * - options (Object): options + * + * Converts the given JsonML into well-formed XML. + * + * The options currently understood are: + * + * - root (Boolean): wether or not the root node should be included in the + * output, or just its children. The default `false` is to not include the + * root itself. + */ + expose.renderJsonML = function(jsonml, options) { + options = options || {}; + // include the root element in the rendered output? + options.root = options.root || false; + var content = []; + if (options.root) { + content.push(render_tree(jsonml)); + } else { + jsonml.shift(); // get rid of the tag + if (jsonml.length && _typeof(jsonml[0]) === "object" && !(jsonml[0] instanceof Array)) { + jsonml.shift(); // get rid of the attributes + } + while (jsonml.length) { + content.push(render_tree(jsonml.shift())); + } + } + return content.join("\n\n"); + }; - return jsonml; -} - - -// merges adjacent text nodes into a single node -function merge_text_nodes( jsonml ) { - // skip the tag name and attribute hash - var i = extract_attr( jsonml ) ? 2 : 1; - - while ( i < jsonml.length ) { - // if it's a string check the next item too - if ( typeof jsonml[ i ] === "string" ) { - if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) { - // merge the second string into the first and remove it - jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ]; - } - else { - ++i; - } + function escapeHTML(text) { + return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } - // if it's not a string recurse - else { - merge_text_nodes( jsonml[ i ] ); - ++i; - } - } -} -} )( (function() { - if ( typeof exports === "undefined" ) { - window.markdown = {}; - return window.markdown; - } - else { - return exports; - } -} )() ); + function render_tree(jsonml) { + // basic case + if (typeof jsonml === "string") { + return escapeHTML(jsonml); + } + var tag = jsonml.shift(), + attributes = {}, + content = []; + if (jsonml.length && _typeof(jsonml[0]) === "object" && !(jsonml[0] instanceof Array)) { + attributes = jsonml.shift(); + } + while (jsonml.length) { + content.push(render_tree(jsonml.shift())); + } + var tag_attrs = ""; + for (var a in attributes) { + tag_attrs += " " + a + '="' + escapeHTML(attributes[a]) + '"'; + } + + // be careful about adding whitespace here for inline elements + if (tag == "img" || tag == "br" || tag == "hr") { + return "<" + tag + tag_attrs + "/>"; + } else { + return "<" + tag + tag_attrs + ">" + content.join("") + ""; + } + } + + function convert_tree_to_html(tree, references, options) { + var i; + options = options || {}; + + // shallow clone + var jsonml = tree.slice(0); + if (typeof options.preprocessTreeNode === "function") { + jsonml = options.preprocessTreeNode(jsonml, references); + } + + // Clone attributes if they exist + var attrs = extract_attr(jsonml); + if (attrs) { + jsonml[1] = {}; + for (i in attrs) { + jsonml[1][i] = attrs[i]; + } + attrs = jsonml[1]; + } + + // basic case + if (typeof jsonml === "string") { + return jsonml; + } + + // convert this node + switch (jsonml[0]) { + case "header": + jsonml[0] = "h" + jsonml[1].level; + delete jsonml[1].level; + break; + case "bulletlist": + jsonml[0] = "ul"; + break; + case "numberlist": + jsonml[0] = "ol"; + break; + case "listitem": + jsonml[0] = "li"; + break; + case "para": + jsonml[0] = "p"; + break; + case "markdown": + jsonml[0] = "html"; + if (attrs) delete attrs.references; + break; + case "code_block": + jsonml[0] = "pre"; + i = attrs ? 2 : 1; + var code = ["code"]; + code.push.apply(code, jsonml.splice(i, jsonml.length - i)); + jsonml[i] = code; + break; + case "inlinecode": + jsonml[0] = "code"; + break; + case "img": + jsonml[1].src = jsonml[1].href; + delete jsonml[1].href; + break; + case "linebreak": + jsonml[0] = "br"; + break; + case "link": + jsonml[0] = "a"; + break; + case "link_ref": + jsonml[0] = "a"; + + // grab this ref and clean up the attribute node + var ref = references[attrs.ref]; + + // if the reference exists, make the link + if (ref) { + delete attrs.ref; + + // add in the href and title, if present + attrs.href = ref.href; + if (ref.title) { + attrs.title = ref.title; + } + + // get rid of the unneeded original text + delete attrs.original; + } + // the reference doesn't exist, so revert to plain text + else { + return attrs.original; + } + break; + case "img_ref": + jsonml[0] = "img"; + + // grab this ref and clean up the attribute node + var ref = references[attrs.ref]; + + // if the reference exists, make the link + if (ref) { + delete attrs.ref; + + // add in the href and title, if present + attrs.src = ref.href; + if (ref.title) { + attrs.title = ref.title; + } + + // get rid of the unneeded original text + delete attrs.original; + } + // the reference doesn't exist, so revert to plain text + else { + return attrs.original; + } + break; + } + + // convert all the children + i = 1; + + // deal with the attribute node, if it exists + if (attrs) { + // if there are keys, skip over it + for (var key in jsonml[1]) { + i = 2; + break; + } + // if there aren't, remove it + if (i === 1) { + jsonml.splice(i, 1); + } + } + for (; i < jsonml.length; ++i) { + jsonml[i] = convert_tree_to_html(jsonml[i], references, options); + } + return jsonml; + } + + // merges adjacent text nodes into a single node + function merge_text_nodes(jsonml) { + // skip the tag name and attribute hash + var i = extract_attr(jsonml) ? 2 : 1; + while (i < jsonml.length) { + // if it's a string check the next item too + if (typeof jsonml[i] === "string") { + if (i + 1 < jsonml.length && typeof jsonml[i + 1] === "string") { + // merge the second string into the first and remove it + jsonml[i] += jsonml.splice(i + 1, 1)[0]; + } else { + ++i; + } + } + // if it's not a string recurse + else { + merge_text_nodes(jsonml[i]); + ++i; + } + } + } +})(function() { + if (typeof exports === "undefined") { + window.markdown = {}; + return window.markdown; + } else { + return exports; + } +}()); \ No newline at end of file diff --git a/shared/html/libs/msgbox/msgbox.js b/shared/html/libs/msgbox/msgbox.js index 5131c6b..af0cd45 100644 --- a/shared/html/libs/msgbox/msgbox.js +++ b/shared/html/libs/msgbox/msgbox.js @@ -39,7 +39,21 @@ var MBRET = { Object.freeze(MBFLAGS); Object.freeze(MBRET); -function GetLocaleStringFromResId(resId) { return Bridge.Resources.byid(resId); } +(function(global) { + try { + var storage = Bridge.External.Storage; + var path = storage.path; + var root = path.getDir(path.program); + var respath = path.combine(root, "reslib.dll"); + var res = Bridge.Resources; + global.respath = respath; + global.getPublicRes = function(resId) { + return res.fromfile(respath, resId); + } + } catch (e) {} +})(this); + +function GetLocaleStringFromResId(resId) { try { return getPublicRes(resId); } catch (e) {} } var msgboxResult = {}; function MessageBox(_lpText, _lpCaption, _uType, _objColor) { @@ -115,9 +129,29 @@ function MessageBoxForJS(_lpText, _lpCaption, _uType, _objColor, _callback) { msgcaption.appendChild(msgtitle); } } - if (_lpText instanceof HTMLElement) { - _lpText.classList.add("notice-text"); - msgcontent.appendChild(_lpText); + if (_lpText instanceof HTMLElement || _lpText instanceof HTMLDivElement || typeof _lpText !== "string") { + try { + _lpText.classList.add("notice-text"); + msgcontent.appendChild(_lpText); + } catch (e) { + if (!IsBlackLabel(_lpText)) { + var msgtext = document.createElement("p"); + msgtext.textContent = _lpText; + msgtext.classList.add("notice-text"); + if (IsBlackLabel(_lpCaption)) { + msgtext.style.marginTop = "0"; + } + msgcontent.appendChild(msgtext); + } else { + var msgtext = document.createElement("p"); + msgtext.innerText = ""; + msgtext.classList.add("notice-text"); + if (IsBlackLabel(_lpCaption)) { + msgtext.style.marginTop = "0"; + } + msgcontent.appendChild(msgtext); + } + } } else { if (!IsBlackLabel(_lpText)) { var msgtext = document.createElement("p"); @@ -330,7 +364,7 @@ MsgBoxObj.prototype._internalCallback = function(returnValue) { * @param {string} swTitle 鏍囬 * @param {MBFLAGS} uType 鏍囧織锛屼娇鐢 MBFLAGS 甯搁噺 * @param {string} swColor 鑳屾櫙棰滆壊鏂囨湰銆 - * @returns + * @returns {Promise} */ function messageBoxAsync(swText, swTitle, uType, swColor, pfCallback) { if (typeof Promise === "undefined") { diff --git a/shared/html/libs/toggle/toggle.css b/shared/html/libs/toggle/toggle.css index e266d3a..8dfca9d 100644 --- a/shared/html/libs/toggle/toggle.css +++ b/shared/html/libs/toggle/toggle.css @@ -7,6 +7,7 @@ max-height: 19px !important; position: relative; display: block; + margin: 5px 0; } .toggle-switch>input[type="checkbox"] { diff --git a/shared/html/settings/appinstaller.html b/shared/html/settings/appinstaller.html index cca83ef..20dc201 100644 --- a/shared/html/settings/appinstaller.html +++ b/shared/html/settings/appinstaller.html @@ -29,6 +29,34 @@ + diff --git a/shared/html/settings/appinstaller/about.html b/shared/html/settings/appinstaller/about.html index 13f062b..ee0cc52 100644 --- a/shared/html/settings/appinstaller/about.html +++ b/shared/html/settings/appinstaller/about.html @@ -28,6 +28,7 @@ + + + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + + diff --git a/shared/html/settings/appinstaller/general.html b/shared/html/settings/appinstaller/general.html index 1870fed..624e6aa 100644 --- a/shared/html/settings/appinstaller/general.html +++ b/shared/html/settings/appinstaller/general.html @@ -159,6 +159,28 @@ })(); +
    +
    + + +
    diff --git a/shared/html/settings/update.html b/shared/html/settings/update.html index fe9367e..fb655c6 100644 --- a/shared/html/settings/update.html +++ b/shared/html/settings/update.html @@ -81,7 +81,8 @@ } #newversion-desc { - white-space: pre-wrap; + /*white-space: pre-wrap;*/ + white-space: normal; } #newversion-desc img { @@ -313,6 +314,13 @@ var newVersionDescShowOrHide = document.getElementById("newversion-desc-showall-hide"); var checkUpdateLoading = document.getElementById("loading-amine"); var stopProcess = false; + var tbpFlags = { + error: 4, + indeterminate: 1, + noProgress: 0, + normal: 2, + paused: 8 + }; function newVersionBlockSizeChanged() { if (stopProcess) return; @@ -426,20 +434,22 @@ var path = storage.path; stopProcess = true; newVersionBlock.parentNode.style.height = "0px"; - checkUpdateText.textContent = getPublicRes(117); + checkUpdateText.textContent = getPublicRes(118); progress.style.display = ""; checkUpdateBlock.style.height = checkUpdateBlock.scrollHeight + "px"; var anime = Windows.UI.Animation; var strutil = Bridge.External.String; + var tbputil = Bridge.External; function setError(error) { //console.error("download error", error); checkUpdateText.textContent = strutil.format(getPublicRes(119), error.reason); progress.style.display = "none"; self.disabled = false; - self.textContent = "閲嶈瘯"; + self.textContent = getPublicRes(132); window.parent.setDisabledForOperation(false); checkUpdateBlock.style.height = checkUpdateBlock.scrollHeight + "px"; + tbputil.tbState = tbpFlags.error; } function setException(e) { @@ -447,6 +457,7 @@ checkUpdateText.textContent = strutil.format(getPublicRes(119), e.message); window.parent.setDisabledForOperation(false); checkUpdateBlock.style.height = checkUpdateBlock.scrollHeight + "px"; + tbputil.tbState = tbpFlags.error; } function setComplete(complete) { @@ -457,16 +468,18 @@ var settingsapppath = path.combine(path.root, "settings.exe"); window.parent.setDisabledForOperation(false); var process = Bridge.External.Process; - process.kill(appinstallerpath, true, false); checkUpdateBlock.style.height = checkUpdateBlock.scrollHeight + "px"; + tbputil.tbState = tbpFlags.noProgress; + process.kill(appinstallerpath, true, false); createProcess(cmdline, tempfile, wndDisplayMode.showNormal, false); process.kill(settingsapppath, true, false); - //Bridge.External.closeWindow(); } progress.value = 0; anime.loading(checkUpdateLoading, true); var tempdir = path.expand("%temp%"); var tempfile = path.combine(tempdir, "InstallerSetup.exe"); + tbputil.tbProgress = 0; + tbputil.tbState = tbpFlags.normal; downloadFile(downloadUrl, tempfile).done( function(complete) { anime.loading(checkUpdateLoading, false); @@ -482,6 +495,7 @@ progress.value = prog.progress; checkUpdateText.textContent = strutil.format(getPublicRes(121), Math.floor(prog.progress), formatBytesSize(prog.received), formatBytesSize(prog.total)); checkUpdateBlock.style.height = checkUpdateBlock.scrollHeight + "px"; + tbputil.tbProgress = prog.progress; } ) } diff --git a/shared/license/license_cn.html b/shared/license/license_cn.html new file mode 100644 index 0000000..10453f9 --- /dev/null +++ b/shared/license/license_cn.html @@ -0,0 +1,87 @@ + + + + + 最终用户许可协议 + + + +

    最终用户许可协议

    + +

    请在安装或使用本软件前仔细阅读本许可协议。

    + +

    一、版权声明

    +

    Copyright © 2025 Windows Modern. 本软件及其源码受版权法保护。

    + +

    二、许可范围

    +

    本软件遵循 MIT 许可协议。您可以在符合 MIT 许可条件下自由使用、复制、修改、分发本软件及其源码,包括商业用途。

    + +

    三、第三方组件

    +

    本软件包含以下第三方开源组件,使用这些组件受其原始许可证约束:

    + + +

    四、免责声明

    +

    本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性、特定用途适用性和非侵权的保证。无论在何种情况下,作者或版权持有人均不对因使用本软件产生的任何直接、间接、偶然或特殊损害负责。

    + +

    五、接受条款

    +

    安装或使用本软件即表示您接受本许可协议。如果您不同意本协议,请不要安装或使用本软件。

    +
    + + diff --git a/shared/license/license_en.html b/shared/license/license_en.html new file mode 100644 index 0000000..0ef7ffa --- /dev/null +++ b/shared/license/license_en.html @@ -0,0 +1,91 @@ + + + + + + End User License Agreement + + + + +

    End User License Agreement

    + +

    Please read this license agreement carefully before installing or using this software.

    + +

    1. Copyright

    +

    Copyright 漏 2025 Windows Modern. This software and its source code are protected by copyright law.

    + +

    2. License

    +

    This software is licensed under the MIT License. You are free to use, copy, modify, and distribute this software and its source code, including for commercial purposes, subject to the terms of the MIT License.

    + +

    3. Third-Party Components

    +

    This software includes the following third-party open source components, which are subject to their original licenses:

    + + +

    4. Disclaimer

    +

    This software is provided "as is", without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be + liable for any damages arising from the use of this software, whether direct, indirect, incidental, or consequential.

    + +

    5. Acceptance

    +

    By installing or using this software, you agree to this license agreement. If you do not agree, do not install or use the software.

    +
    + + + \ No newline at end of file diff --git a/shared/locale/resources.xml b/shared/locale/resources.xml index e0efc7e..42a3e12 100644 --- a/shared/locale/resources.xml +++ b/shared/locale/resources.xml @@ -113,7 +113,6 @@ 闂滈枆 Cishile - On @@ -227,4 +226,8 @@ 闁嬪暉 Vuliwe + + license_cn.html + license_en.html + \ No newline at end of file