diff --git a/.gitignore b/.gitignore index 0d8b673..deedef9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ js/node_modules js/dist vendor/ composer.lock +analysis/* diff --git a/composer.json b/composer.json index f2013a7..97b213d 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "blomstra/search", + "name": "sbp/flarum-search", "description": "Replaces Flarum search with one powered by an elastic search server.", "keywords": [ "extension", @@ -11,16 +11,20 @@ "elasticsearch" ], "support": { - "email": "helpdesk@blomstra.net", + "email": "mueh+git@sbp.de", "forum": "https://blomstra.community/t/ext-search" }, "type": "flarum-extension", "license": "MIT", "authors": [ + { + "name": "sbp", + "email": "info@sbp.de" + }, { "name": "Team Blomstra", "email": "development@blomstra.net", - "role": "Developer" + "role": "Original Developer" } ], "require": { diff --git a/extend.php b/extend.php index 3dec9f6..4bdba7d 100644 --- a/extend.php +++ b/extend.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search; +namespace Sbp\Search; use Flarum\Extend as Flarum; @@ -26,13 +26,16 @@ return [ new Flarum\Locales(__DIR__.'/resources/locale'), (new Flarum\Routes('api')) - ->get('/blomstra/search/{type}', 'blomstra.search', Api\Controllers\SearchController::class) - ->put('/blomstra/search/index', 'blomstra.search.index', Api\Controllers\IndexController::class), + ->get('/sbp/search/{type}', 'sbp.search', Api\Controllers\SearchController::class) + ->put('/sbp/search/index', 'sbp.search.index', Api\Controllers\IndexController::class), (new Flarum\Console()) ->command(Commands\BuildCommand::class), (new Flarum\Settings()) ->default('blomstra-search.search-discussion-subjects', true) ->default('blomstra-search.search-post-bodies', true) - ->default('blomstra-search.min-search-length', Commands\BuildCommand::DEFAULT_MIN_SEARCH_LENGTH), + ->default('blomstra-search.min-search-length', Commands\BuildCommand::DEFAULT_MIN_SEARCH_LENGTH) + ->default('blomstra-search.stem-exclusion', '') + ->serializeToForum('blomstraSearchMinLength', 'blomstra-search.min-search-length', 'intval') + ->serializeToForum('blomstraSearchPostBodies', 'blomstra-search.search-post-bodies', fn ($v) => (bool) $v, true), ]; diff --git a/js/dist/admin.js b/js/dist/admin.js index acbf469..274d651 100644 --- a/js/dist/admin.js +++ b/js/dist/admin.js @@ -1,2 +1,2 @@ -(()=>{var t={n:a=>{var e=a&&a.__esModule?()=>a.default:()=>a;return t.d(e,{a:e}),e},d:(a,e)=>{for(var r in e)t.o(e,r)&&!t.o(a,r)&&Object.defineProperty(a,r,{enumerable:!0,get:e[r]})},o:(t,a)=>Object.prototype.hasOwnProperty.call(t,a)};(()=>{"use strict";function a(t,e){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,a){return t.__proto__=a,t},a(t,e)}const e=flarum.core.compat["admin/app"];var r=t.n(e);const n=flarum.core.compat["common/extend"],s=flarum.core.compat["admin/components/DashboardPage"];var i=t.n(s);const o=flarum.core.compat["admin/components/DashboardWidget"];var l=t.n(o);const c=flarum.core.compat["common/components/Alert"];var h=t.n(c),d=function(t){var e,n;function s(){return t.apply(this,arguments)||this}n=t,(e=s).prototype=Object.create(n.prototype),e.prototype.constructor=e,a(e,n);var i=s.prototype;return i.className=function(){return"ReindexWarningWidget"},i.content=function(){return m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle",title:r().translator.trans("blomstra-search.admin.reindex-required.title")},r().translator.trans("blomstra-search.admin.reindex-required.detail"))},s}(l());r().initializers.add("blomstra-search",function(){var t=r().data.settings["blomstra-search.active-index"],a=r().data.settings["blomstra-search.index-compatible"];t&&"v2"!==a&&(0,n.extend)(i().prototype,"availableWidgets",function(t){t.add("blomstra-search-reindex",m(d),110)});var e=new Map;["arabic","armenian","basque","bengali","brazilian","bulgarian","catalan","cjk","czech","danish","dutch","english","estonian","finnish","french","galician","german","greek","hindi","hungarian","indonesian","irish","italian","latvian","lithuanian","norwegian","persian","portuguese","romanian","russian","sorani","spanish","swedish","turkish","thai"].forEach(function(t){e.set(t,t)}),r().extensionData.for("blomstra-search").registerSetting(function(){var a=r().data.settings["blomstra-search.indexed-analyzer"];if(!t||!a)return null;var e=this.setting("blomstra-search.analyzer-language")()||"english",n=this.setting("blomstra-search.min-search-length")(),s=String(r().data.settings["blomstra-search.indexed-min-search-length"]||r().data.settings["blomstra-search.min-search-length"]);return e===a&&n===s?null:m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle"},r().translator.trans("blomstra-search.admin.index-settings-changed"))}).registerSetting({setting:"blomstra-search.elastic-endpoint",label:r().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:r().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:r().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:r().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.analyzer-language",label:r().translator.trans("blomstra-search.admin.analyzer.label"),help:r().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:Object.fromEntries(e.entries()),default:"english"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:r().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:r().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"}).registerSetting({setting:"blomstra-search.min-search-length",label:r().translator.trans("blomstra-search.admin.min-search-length.label"),help:r().translator.trans("blomstra-search.admin.min-search-length.help"),type:"select",options:{1:"1",2:"2",3:"3",4:"4"},default:r().data.settings["blomstra-search.min-search-length"]})})})(),module.exports={}})(); +(()=>{var a={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return a.d(e,{a:e}),e},d:(t,e)=>{for(var n in e)a.o(e,n)&&!a.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o:(a,t)=>Object.prototype.hasOwnProperty.call(a,t)};(()=>{"use strict";function t(a,e){return t=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(a,t){return a.__proto__=t,a},t(a,e)}const e=flarum.core.compat["admin/app"];var n=a.n(e);const r=flarum.core.compat["common/extend"],s=flarum.core.compat["admin/components/DashboardPage"];var i=a.n(s);const l=flarum.core.compat["admin/components/DashboardWidget"];var o=a.n(l);const c=flarum.core.compat["common/components/Alert"];var h=a.n(c),d=function(a){var e,r;function s(){return a.apply(this,arguments)||this}r=a,(e=s).prototype=Object.create(r.prototype),e.prototype.constructor=e,t(e,r);var i=s.prototype;return i.className=function(){return"ReindexWarningWidget"},i.content=function(){return m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle",title:n().translator.trans("blomstra-search.admin.reindex-required.title")},n().translator.trans("blomstra-search.admin.reindex-required.detail"))},s}(o());n().initializers.add("sbp-search",function(){var a=n().data.settings["blomstra-search.active-index"],t=n().data.settings["blomstra-search.index-compatible"];a&&"v3"!==t&&(0,r.extend)(i().prototype,"availableWidgets",function(a){a.add("blomstra-search-reindex",m(d),110)}),n().extensionData.for("blomstra-search").registerSetting(function(){var t=n().data.settings["blomstra-search.indexed-analyzer"];if(!a||!t)return null;var e=this.setting("blomstra-search.analyzer-language")()||"english",r=this.setting("blomstra-search.stem-exclusion")()||"",s=n().data.settings["blomstra-search.indexed-stem-exclusion"]||"";return e===t&&r===s?null:m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle"},n().translator.trans("blomstra-search.admin.index-settings-changed"))}).registerSetting({setting:"blomstra-search.elastic-endpoint",label:n().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:n().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:n().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:n().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:n().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:n().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"}).registerSetting({setting:"blomstra-search.analyzer-language",label:n().translator.trans("blomstra-search.admin.analyzer.label"),help:n().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:{arabic:"Arabic",armenian:"Armenian",basque:"Basque",bengali:"Bengali",brazilian:"Brazilian Portuguese",bulgarian:"Bulgarian",catalan:"Catalan",cjk:"CJK (Chinese, Japanese, Korean)",czech:"Czech",danish:"Danish",dutch:"Dutch",english:"English",estonian:"Estonian",finnish:"Finnish",french:"French",galician:"Galician",german:"German",greek:"Greek",hindi:"Hindi",hungarian:"Hungarian",indonesian:"Indonesian",irish:"Irish",italian:"Italian",latvian:"Latvian",lithuanian:"Lithuanian",norwegian:"Norwegian",persian:"Persian",portuguese:"Portuguese",romanian:"Romanian",russian:"Russian",sorani:"Sorani (Kurdish)",spanish:"Spanish",standard:"Standard (generic)",swedish:"Swedish",turkish:"Turkish",thai:"Thai"},default:"english"}).registerSetting(function(){var a="cjk"===(this.setting("blomstra-search.analyzer-language")()||"english");return this.buildSettingComponent({setting:"blomstra-search.stem-exclusion",type:"textarea",label:n().translator.trans("blomstra-search.admin.settings.stem-exclusion.label"),help:n().translator.trans("blomstra-search.admin.settings.stem-exclusion.help"),disabled:a})}).registerSetting(function(){var a="cjk"===(this.setting("blomstra-search.analyzer-language")()||"english");return this.buildSettingComponent({setting:"blomstra-search.min-search-length",label:n().translator.trans("blomstra-search.admin.min-search-length.label"),help:n().translator.trans("blomstra-search.admin.min-search-length.help"),type:"select",options:{1:"1",2:"2",3:"3",4:"4"},default:n().data.settings["blomstra-search.min-search-length"],disabled:a})})})})(),module.exports={}})(); //# sourceMappingURL=admin.js.map \ No newline at end of file diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map index bd0ccbd..010293a 100644 --- a/js/dist/admin.js.map +++ b/js/dist/admin.js.map @@ -1 +1 @@ -{"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@blomstra/search/webpack/bootstrap","webpack://@blomstra/search/webpack/runtime/compat get default export","webpack://@blomstra/search/webpack/runtime/define property getters","webpack://@blomstra/search/webpack/runtime/hasOwnProperty shorthand","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@blomstra/search/external root \"flarum.core.compat['admin/app']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/extend']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardPage']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardWidget']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/components/Alert']\"","webpack://@blomstra/search/./src/admin/index.ts","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","function _setPrototypeOf(t, e) {\n return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {\n return t.__proto__ = e, t;\n }, _setPrototypeOf(t, e);\n}\nexport { _setPrototypeOf as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardWidget'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Alert'];","import app from 'flarum/admin/app';\nimport { extend } from 'flarum/common/extend';\nimport DashboardPage from 'flarum/admin/components/DashboardPage';\nimport DashboardWidget from 'flarum/admin/components/DashboardWidget';\nimport Alert from 'flarum/common/components/Alert';\n\nconst REQUIRED_INDEX_COMPAT = 'v2';\n\nclass ReindexWarningWidget extends DashboardWidget {\n className() {\n return 'ReindexWarningWidget';\n }\n\n content() {\n return m(\n Alert,\n {\n type: 'warning',\n dismissible: false,\n icon: 'fas fa-exclamation-triangle',\n title: app.translator.trans('blomstra-search.admin.reindex-required.title'),\n },\n app.translator.trans('blomstra-search.admin.reindex-required.detail')\n );\n }\n}\n\napp.initializers.add('blomstra-search', () => {\n const activeIndex = app.data.settings['blomstra-search.active-index'];\n const compatVersion = app.data.settings['blomstra-search.index-compatible'];\n\n if (activeIndex && compatVersion !== REQUIRED_INDEX_COMPAT) {\n extend(DashboardPage.prototype, 'availableWidgets', function (items) {\n items.add('blomstra-search-reindex', m(ReindexWarningWidget), 110);\n });\n }\n\n const languages = new Map();\n [\n 'arabic',\n 'armenian',\n 'basque',\n 'bengali',\n 'brazilian',\n 'bulgarian',\n 'catalan',\n 'cjk',\n 'czech',\n 'danish',\n 'dutch',\n 'english',\n 'estonian',\n 'finnish',\n 'french',\n 'galician',\n 'german',\n 'greek',\n 'hindi',\n 'hungarian',\n 'indonesian',\n 'irish',\n 'italian',\n 'latvian',\n 'lithuanian',\n 'norwegian',\n 'persian',\n 'portuguese',\n 'romanian',\n 'russian',\n 'sorani',\n 'spanish',\n 'swedish',\n 'turkish',\n 'thai',\n ].forEach((language) => {\n languages.set(language, language);\n });\n\n app.extensionData\n .for('blomstra-search')\n .registerSetting(function (this: any) {\n // No index yet, or first build predates this tracking — stay silent.\n const indexedAnalyzer = app.data.settings['blomstra-search.indexed-analyzer'];\n if (!activeIndex || !indexedAnalyzer) return null;\n\n const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english';\n const currentMinLength = this.setting('blomstra-search.min-search-length')();\n const indexedMinLength = String(\n app.data.settings['blomstra-search.indexed-min-search-length'] || app.data.settings['blomstra-search.min-search-length']\n );\n\n if (currentAnalyzer === indexedAnalyzer && currentMinLength === indexedMinLength) return null;\n\n return m(\n Alert,\n { type: 'warning', dismissible: false, icon: 'fas fa-exclamation-triangle' },\n app.translator.trans('blomstra-search.admin.index-settings-changed')\n );\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-endpoint',\n label: app.translator.trans('blomstra-search.admin.elastic-endpoint'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-username',\n label: app.translator.trans('blomstra-search.admin.elastic-username'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-password',\n label: app.translator.trans('blomstra-search.admin.elastic-password'),\n type: 'password',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-index',\n label: app.translator.trans('blomstra-search.admin.elastic-index'),\n default: 'flarum',\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.analyzer-language',\n label: app.translator.trans('blomstra-search.admin.analyzer.label'),\n help: app.translator.trans('blomstra-search.admin.analyzer.help'),\n type: 'select',\n options: Object.fromEntries(languages.entries()),\n default: 'english',\n })\n .registerSetting({\n setting: 'blomstra-search.search-discussion-subjects',\n label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.search-post-bodies',\n label: app.translator.trans('blomstra-search.admin.search-post-bodies'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.min-search-length',\n label: app.translator.trans('blomstra-search.admin.min-search-length.label'),\n help: app.translator.trans('blomstra-search.admin.min-search-length.help'),\n type: 'select',\n options: { '1': '1', '2': '2', '3': '3', '4': '4' },\n default: app.data.settings['blomstra-search.min-search-length'],\n });\n});\n","import setPrototypeOf from \"./setPrototypeOf.js\";\nfunction _inheritsLoose(t, o) {\n t.prototype = Object.create(o.prototype), t.prototype.constructor = t, setPrototypeOf(t, o);\n}\nexport { _inheritsLoose as default };"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","_setPrototypeOf","t","e","setPrototypeOf","bind","__proto__","flarum","core","compat","ReindexWarningWidget","_DashboardWidget","apply","arguments","create","constructor","_proto","className","content","m","Alert","type","dismissible","icon","title","app","trans","DashboardWidget","add","activeIndex","settings","compatVersion","extend","DashboardPage","items","languages","Map","forEach","language","set","registerSetting","indexedAnalyzer","currentAnalyzer","this","setting","currentMinLength","indexedMinLength","String","label","default","help","options","fromEntries","entries"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@blomstra/search/webpack/bootstrap","webpack://@blomstra/search/webpack/runtime/compat get default export","webpack://@blomstra/search/webpack/runtime/define property getters","webpack://@blomstra/search/webpack/runtime/hasOwnProperty shorthand","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@blomstra/search/external root \"flarum.core.compat['admin/app']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/extend']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardPage']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardWidget']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/components/Alert']\"","webpack://@blomstra/search/./src/admin/index.ts","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","function _setPrototypeOf(t, e) {\n return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {\n return t.__proto__ = e, t;\n }, _setPrototypeOf(t, e);\n}\nexport { _setPrototypeOf as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardWidget'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Alert'];","import app from 'flarum/admin/app';\nimport { extend } from 'flarum/common/extend';\nimport DashboardPage from 'flarum/admin/components/DashboardPage';\nimport DashboardWidget from 'flarum/admin/components/DashboardWidget';\nimport Alert from 'flarum/common/components/Alert';\n\nconst REQUIRED_INDEX_COMPAT = 'v3';\n\nclass ReindexWarningWidget extends DashboardWidget {\n className() {\n return 'ReindexWarningWidget';\n }\n\n content() {\n return m(\n Alert,\n {\n type: 'warning',\n dismissible: false,\n icon: 'fas fa-exclamation-triangle',\n title: app.translator.trans('blomstra-search.admin.reindex-required.title'),\n },\n app.translator.trans('blomstra-search.admin.reindex-required.detail')\n );\n }\n}\n\napp.initializers.add('sbp-search', () => {\n const activeIndex = app.data.settings['blomstra-search.active-index'];\n const compatVersion = app.data.settings['blomstra-search.index-compatible'];\n\n if (activeIndex && compatVersion !== REQUIRED_INDEX_COMPAT) {\n extend(DashboardPage.prototype, 'availableWidgets', function (items) {\n items.add('blomstra-search-reindex', m(ReindexWarningWidget), 110);\n });\n }\n\n const languages: Record = {\n arabic: 'Arabic',\n armenian: 'Armenian',\n basque: 'Basque',\n bengali: 'Bengali',\n brazilian: 'Brazilian Portuguese',\n bulgarian: 'Bulgarian',\n catalan: 'Catalan',\n cjk: 'CJK (Chinese, Japanese, Korean)',\n czech: 'Czech',\n danish: 'Danish',\n dutch: 'Dutch',\n english: 'English',\n estonian: 'Estonian',\n finnish: 'Finnish',\n french: 'French',\n galician: 'Galician',\n german: 'German',\n greek: 'Greek',\n hindi: 'Hindi',\n hungarian: 'Hungarian',\n indonesian: 'Indonesian',\n irish: 'Irish',\n italian: 'Italian',\n latvian: 'Latvian',\n lithuanian: 'Lithuanian',\n norwegian: 'Norwegian',\n persian: 'Persian',\n portuguese: 'Portuguese',\n romanian: 'Romanian',\n russian: 'Russian',\n sorani: 'Sorani (Kurdish)',\n spanish: 'Spanish',\n standard: 'Standard (generic)',\n swedish: 'Swedish',\n turkish: 'Turkish',\n thai: 'Thai',\n };\n\n app.extensionData\n .for('blomstra-search')\n .registerSetting(function (this: any) {\n // No index yet, or first build predates this tracking — stay silent.\n const indexedAnalyzer = app.data.settings['blomstra-search.indexed-analyzer'];\n if (!activeIndex || !indexedAnalyzer) return null;\n\n const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english';\n const currentStemExclusion = this.setting('blomstra-search.stem-exclusion')() || '';\n const indexedStemExclusion = app.data.settings['blomstra-search.indexed-stem-exclusion'] || '';\n\n if (currentAnalyzer === indexedAnalyzer && currentStemExclusion === indexedStemExclusion) return null;\n\n return m(\n Alert,\n { type: 'warning', dismissible: false, icon: 'fas fa-exclamation-triangle' },\n app.translator.trans('blomstra-search.admin.index-settings-changed')\n );\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-endpoint',\n label: app.translator.trans('blomstra-search.admin.elastic-endpoint'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-username',\n label: app.translator.trans('blomstra-search.admin.elastic-username'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-password',\n label: app.translator.trans('blomstra-search.admin.elastic-password'),\n type: 'password',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-index',\n label: app.translator.trans('blomstra-search.admin.elastic-index'),\n default: 'flarum',\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.search-discussion-subjects',\n label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.search-post-bodies',\n label: app.translator.trans('blomstra-search.admin.search-post-bodies'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.analyzer-language',\n label: app.translator.trans('blomstra-search.admin.analyzer.label'),\n help: app.translator.trans('blomstra-search.admin.analyzer.help'),\n type: 'select',\n options: languages,\n default: 'english',\n })\n .registerSetting(function (this: any) {\n const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk';\n return this.buildSettingComponent({\n setting: 'blomstra-search.stem-exclusion',\n type: 'textarea',\n label: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.label'),\n help: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.help'),\n disabled: isCjk,\n });\n })\n .registerSetting(function (this: any) {\n const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk';\n return this.buildSettingComponent({\n setting: 'blomstra-search.min-search-length',\n label: app.translator.trans('blomstra-search.admin.min-search-length.label'),\n help: app.translator.trans('blomstra-search.admin.min-search-length.help'),\n type: 'select',\n options: { '1': '1', '2': '2', '3': '3', '4': '4' },\n default: app.data.settings['blomstra-search.min-search-length'],\n disabled: isCjk,\n });\n });\n});\n","import setPrototypeOf from \"./setPrototypeOf.js\";\nfunction _inheritsLoose(t, o) {\n t.prototype = Object.create(o.prototype), t.prototype.constructor = t, setPrototypeOf(t, o);\n}\nexport { _inheritsLoose as default };"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","_setPrototypeOf","t","e","setPrototypeOf","bind","__proto__","flarum","core","compat","ReindexWarningWidget","_DashboardWidget","apply","arguments","create","constructor","_proto","className","content","m","Alert","type","dismissible","icon","title","app","trans","DashboardWidget","add","activeIndex","settings","compatVersion","extend","DashboardPage","items","registerSetting","indexedAnalyzer","currentAnalyzer","this","setting","currentStemExclusion","indexedStemExclusion","label","default","help","options","arabic","armenian","basque","bengali","brazilian","bulgarian","catalan","cjk","czech","danish","dutch","english","estonian","finnish","french","galician","german","greek","hindi","hungarian","indonesian","irish","italian","latvian","lithuanian","norwegian","persian","portuguese","romanian","russian","sorani","spanish","standard","swedish","turkish","thai","isCjk","buildSettingComponent","disabled"],"sourceRoot":""} \ No newline at end of file diff --git a/js/dist/forum.js b/js/dist/forum.js index a9c0a3a..fdaccef 100644 --- a/js/dist/forum.js +++ b/js/dist/forum.js @@ -1,3 +1,3 @@ /*! For license information please see forum.js.LICENSE.txt */ -(()=>{var t={163(t){t.exports=function(t,e){this.v=t,this.k=e},t.exports.__esModule=!0,t.exports.default=t.exports},110(t,e,r){var n=r(397);function o(){var e,r,s="function"==typeof Symbol?Symbol:{},u=s.iterator||"@@iterator",a=s.toStringTag||"@@toStringTag";function i(t,o,s,u){var a=o&&o.prototype instanceof p?o:p,i=Object.create(a.prototype);return n(i,"_invoke",function(t,n,o){var s,u,a,i=0,p=o||[],l=!1,f={p:0,n:0,v:e,a:m,f:m.bind(e,4),d:function(t,r){return s=t,u=0,a=e,f.n=r,c}};function m(t,n){for(u=t,a=n,r=0;!l&&i&&!o&&r3?(o=d===n)&&(a=s[(u=s[4])?5:(u=3,3)],s[4]=s[5]=e):s[0]<=m&&((o=t<2&&mn||n>d)&&(s[4]=t,s[5]=n,f.n=d,u=0))}if(o||t>1)return c;throw l=!0,n}return function(o,p,d){if(i>1)throw TypeError("Generator is already running");for(l&&1===p&&m(p,d),u=p,a=d;(r=u<2?e:a)||!l;){s||(u?u<3?(u>1&&(f.n=-1),m(u,a)):f.n=a:f.v=a);try{if(i=2,s){if(u||(o="next"),r=s[o]){if(!(r=r.call(s,a)))throw TypeError("iterator result is not an object");if(!r.done)return r;a=r.value,u<2&&(u=0)}else 1===u&&(r=s.return)&&r.call(s),u<2&&(a=TypeError("The iterator does not provide a '"+o+"' method"),u=1);s=e}else if((r=(l=f.n<0)?a:t.call(n,f))!==c)break}catch(t){s=e,u=1,a=t}finally{i=1}}return{value:r,done:l}}}(t,s,u),!0),i}var c={};function p(){}function l(){}function f(){}r=Object.getPrototypeOf;var m=[][u]?r(r([][u]())):(n(r={},u,function(){return this}),r),d=f.prototype=p.prototype=Object.create(m);function v(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,f):(t.__proto__=f,n(t,a,"GeneratorFunction")),t.prototype=Object.create(d),t}return l.prototype=f,n(d,"constructor",f),n(f,"constructor",l),l.displayName="GeneratorFunction",n(f,a,"GeneratorFunction"),n(d),n(d,a,"Generator"),n(d,u,function(){return this}),n(d,"toString",function(){return"[object Generator]"}),(t.exports=o=function(){return{w:i,m:v}},t.exports.__esModule=!0,t.exports.default=t.exports)()}t.exports=o,t.exports.__esModule=!0,t.exports.default=t.exports},996(t,e,r){var n=r(312);t.exports=function(t,e,r,o,s){var u=n(t,e,r,o,s);return u.next().then(function(t){return t.done?t.value:u.next()})},t.exports.__esModule=!0,t.exports.default=t.exports},312(t,e,r){var n=r(110),o=r(166);t.exports=function(t,e,r,s,u){return new o(n().w(t,e,r,s),u||Promise)},t.exports.__esModule=!0,t.exports.default=t.exports},166(t,e,r){var n=r(163),o=r(397);t.exports=function t(e,r){function s(t,o,u,a){try{var i=e[t](o),c=i.value;return c instanceof n?r.resolve(c.v).then(function(t){s("next",t,u,a)},function(t){s("throw",t,u,a)}):r.resolve(c).then(function(t){i.value=t,u(i)},function(t){return s("throw",t,u,a)})}catch(t){a(t)}}var u;this.next||(o(t.prototype),o(t.prototype,"function"==typeof Symbol&&Symbol.asyncIterator||"@asyncIterator",function(){return this})),o(this,"_invoke",function(t,e,n){function o(){return new r(function(e,r){s(t,n,e,r)})}return u=u?u.then(o,o):o()},!0)},t.exports.__esModule=!0,t.exports.default=t.exports},397(t){function e(r,n,o,s){var u=Object.defineProperty;try{u({},"",{})}catch(r){u=0}t.exports=e=function(t,r,n,o){function s(r,n){e(t,r,function(t){return this._invoke(r,n,t)})}r?u?u(t,r,{value:n,enumerable:!o,configurable:!o,writable:!o}):t[r]=n:(s("next",0),s("throw",1),s("return",2))},t.exports.__esModule=!0,t.exports.default=t.exports,e(r,n,o,s)}t.exports=e,t.exports.__esModule=!0,t.exports.default=t.exports},766(t){t.exports=function(t){var e=Object(t),r=[];for(var n in e)r.unshift(n);return function t(){for(;r.length;)if((n=r.pop())in e)return t.value=n,t.done=!1,t;return t.done=!0,t}},t.exports.__esModule=!0,t.exports.default=t.exports},24(t,e,r){var n=r(163),o=r(110),s=r(996),u=r(312),a=r(166),i=r(766),c=r(760);function p(){"use strict";var e=o(),r=e.m(p),l=(Object.getPrototypeOf?Object.getPrototypeOf(r):r.__proto__).constructor;function f(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===l||"GeneratorFunction"===(e.displayName||e.name))}var m={throw:1,return:2,break:3,continue:3};function d(t){var e,r;return function(n){e||(e={stop:function(){return r(n.a,2)},catch:function(){return n.v},abrupt:function(t,e){return r(n.a,m[t],e)},delegateYield:function(t,o,s){return e.resultName=o,r(n.d,c(t),s)},finish:function(t){return r(n.f,t)}},r=function(t,r,o){n.p=e.prev,n.n=e.next;try{return t(r,o)}finally{e.next=n.n}}),e.resultName&&(e[e.resultName]=n.v,e.resultName=void 0),e.sent=n.v,e.next=n.n;try{return t.call(this,e)}finally{n.p=e.prev,n.n=e.next}}}return(t.exports=p=function(){return{wrap:function(t,r,n,o){return e.w(d(t),r,n,o&&o.reverse())},isGeneratorFunction:f,mark:e.m,awrap:function(t,e){return new n(t,e)},AsyncIterator:a,async:function(t,e,r,n,o){return(f(e)?u:s)(d(t),e,r,n,o)},keys:i,values:c}},t.exports.__esModule=!0,t.exports.default=t.exports)()}t.exports=p,t.exports.__esModule=!0,t.exports.default=t.exports},760(t,e,r){var n=r(735).default;t.exports=function(t){if(null!=t){var e=t["function"==typeof Symbol&&Symbol.iterator||"@@iterator"],r=0;if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length))return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}}}throw new TypeError(n(t)+" is not iterable")},t.exports.__esModule=!0,t.exports.default=t.exports},735(t){function e(r){return t.exports=e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t.exports.__esModule=!0,t.exports.default=t.exports,e(r)}t.exports=e,t.exports.__esModule=!0,t.exports.default=t.exports},183(t,e,r){var n=r(24)();t.exports=n;try{regeneratorRuntime=n}catch(t){"object"==typeof globalThis?globalThis.regeneratorRuntime=n:Function("r","regeneratorRuntime = r")(n)}}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var s=e[n]={exports:{}};return t[n](s,s.exports,r),s.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";const t=flarum.core.compat["forum/app"];var e=r.n(t);const n=flarum.core.compat["forum/components/Search"];var o=r.n(n);const s=flarum.core.compat["common/extend"];function u(t,e,r,n,o,s,u){try{var a=t[s](u),i=a.value}catch(t){return void r(t)}a.done?e(i):Promise.resolve(i).then(n,o)}function a(t){return function(){var e=this,r=arguments;return new Promise(function(n,o){var s=t.apply(e,r);function a(t){u(s,n,o,a,i,"next",t)}function i(t){u(s,n,o,a,i,"throw",t)}a(void 0)})}}var i=r(183),c=r.n(i);const p=flarum.core.compat["common/helpers/highlight"];var l=r.n(p);const f=flarum.core.compat["common/components/LinkButton"];var d=r.n(f);const v=flarum.core.compat["common/components/Link"];var h=r.n(v),x=function(){function t(){this.results=new Map,this.type="discussions"}var r=t.prototype;return r.search=function(){var t=a(c().mark(function t(r){var n,o,s,u;return c().wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return r=r.toLowerCase(),this.results.set(r,[]),n={filter:{q:r},page:{limit:3},include:"mostRelevantPost"},o=e().forum.attribute("apiUrl")+"/blomstra/search/"+this.type,t.next=6,e().request({params:n,url:o,method:"GET"});case 6:s=t.sent,u=e().store.pushPayload(s),this.results.set(r,u);case 9:case"end":return t.stop()}},t,this)}));return function(e){return t.apply(this,arguments)}}(),r.view=function(t){var r=this;t=t.toLowerCase();var n=(this.results.get(t)||[]).map(function(n){var o=n.mostRelevantPost();return m("li",{className:"DiscussionSearchResult","data-index":""+r.type+n.id()},m(h(),{href:e().route.discussion(n,o&&o.number())},m("div",{className:"DiscussionSearchResult-title"},l()(n.title(),t)),!!o&&m("div",{className:"DiscussionSearchResult-excerpt"},l()(o.contentPlain(),t,100))))});return[m("li",{className:"Dropdown-header"},e().translator.trans("core.forum.search.discussions_heading")),m("li",null,m(d(),{icon:"fas fa-search",href:e().route("index",{q:t})},e().translator.trans("core.forum.search.all_discussions_button",{query:t})))].concat(n)},t}();function y(){return y=Object.assign?Object.assign.bind():function(t){for(var e=1;e{var t={163(t){t.exports=function(t,e){this.v=t,this.k=e},t.exports.__esModule=!0,t.exports.default=t.exports},110(t,e,r){var n=r(397);function o(){var e,r,s="function"==typeof Symbol?Symbol:{},u=s.iterator||"@@iterator",a=s.toStringTag||"@@toStringTag";function i(t,o,s,u){var a=o&&o.prototype instanceof p?o:p,i=Object.create(a.prototype);return n(i,"_invoke",function(t,n,o){var s,u,a,i=0,p=o||[],l=!1,f={p:0,n:0,v:e,a:m,f:m.bind(e,4),d:function(t,r){return s=t,u=0,a=e,f.n=r,c}};function m(t,n){for(u=t,a=n,r=0;!l&&i&&!o&&r3?(o=d===n)&&(a=s[(u=s[4])?5:(u=3,3)],s[4]=s[5]=e):s[0]<=m&&((o=t<2&&mn||n>d)&&(s[4]=t,s[5]=n,f.n=d,u=0))}if(o||t>1)return c;throw l=!0,n}return function(o,p,d){if(i>1)throw TypeError("Generator is already running");for(l&&1===p&&m(p,d),u=p,a=d;(r=u<2?e:a)||!l;){s||(u?u<3?(u>1&&(f.n=-1),m(u,a)):f.n=a:f.v=a);try{if(i=2,s){if(u||(o="next"),r=s[o]){if(!(r=r.call(s,a)))throw TypeError("iterator result is not an object");if(!r.done)return r;a=r.value,u<2&&(u=0)}else 1===u&&(r=s.return)&&r.call(s),u<2&&(a=TypeError("The iterator does not provide a '"+o+"' method"),u=1);s=e}else if((r=(l=f.n<0)?a:t.call(n,f))!==c)break}catch(t){s=e,u=1,a=t}finally{i=1}}return{value:r,done:l}}}(t,s,u),!0),i}var c={};function p(){}function l(){}function f(){}r=Object.getPrototypeOf;var m=[][u]?r(r([][u]())):(n(r={},u,function(){return this}),r),d=f.prototype=p.prototype=Object.create(m);function v(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,f):(t.__proto__=f,n(t,a,"GeneratorFunction")),t.prototype=Object.create(d),t}return l.prototype=f,n(d,"constructor",f),n(f,"constructor",l),l.displayName="GeneratorFunction",n(f,a,"GeneratorFunction"),n(d),n(d,a,"Generator"),n(d,u,function(){return this}),n(d,"toString",function(){return"[object Generator]"}),(t.exports=o=function(){return{w:i,m:v}},t.exports.__esModule=!0,t.exports.default=t.exports)()}t.exports=o,t.exports.__esModule=!0,t.exports.default=t.exports},996(t,e,r){var n=r(312);t.exports=function(t,e,r,o,s){var u=n(t,e,r,o,s);return u.next().then(function(t){return t.done?t.value:u.next()})},t.exports.__esModule=!0,t.exports.default=t.exports},312(t,e,r){var n=r(110),o=r(166);t.exports=function(t,e,r,s,u){return new o(n().w(t,e,r,s),u||Promise)},t.exports.__esModule=!0,t.exports.default=t.exports},166(t,e,r){var n=r(163),o=r(397);t.exports=function t(e,r){function s(t,o,u,a){try{var i=e[t](o),c=i.value;return c instanceof n?r.resolve(c.v).then(function(t){s("next",t,u,a)},function(t){s("throw",t,u,a)}):r.resolve(c).then(function(t){i.value=t,u(i)},function(t){return s("throw",t,u,a)})}catch(t){a(t)}}var u;this.next||(o(t.prototype),o(t.prototype,"function"==typeof Symbol&&Symbol.asyncIterator||"@asyncIterator",function(){return this})),o(this,"_invoke",function(t,e,n){function o(){return new r(function(e,r){s(t,n,e,r)})}return u=u?u.then(o,o):o()},!0)},t.exports.__esModule=!0,t.exports.default=t.exports},397(t){function e(r,n,o,s){var u=Object.defineProperty;try{u({},"",{})}catch(r){u=0}t.exports=e=function(t,r,n,o){function s(r,n){e(t,r,function(t){return this._invoke(r,n,t)})}r?u?u(t,r,{value:n,enumerable:!o,configurable:!o,writable:!o}):t[r]=n:(s("next",0),s("throw",1),s("return",2))},t.exports.__esModule=!0,t.exports.default=t.exports,e(r,n,o,s)}t.exports=e,t.exports.__esModule=!0,t.exports.default=t.exports},766(t){t.exports=function(t){var e=Object(t),r=[];for(var n in e)r.unshift(n);return function t(){for(;r.length;)if((n=r.pop())in e)return t.value=n,t.done=!1,t;return t.done=!0,t}},t.exports.__esModule=!0,t.exports.default=t.exports},24(t,e,r){var n=r(163),o=r(110),s=r(996),u=r(312),a=r(166),i=r(766),c=r(760);function p(){"use strict";var e=o(),r=e.m(p),l=(Object.getPrototypeOf?Object.getPrototypeOf(r):r.__proto__).constructor;function f(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===l||"GeneratorFunction"===(e.displayName||e.name))}var m={throw:1,return:2,break:3,continue:3};function d(t){var e,r;return function(n){e||(e={stop:function(){return r(n.a,2)},catch:function(){return n.v},abrupt:function(t,e){return r(n.a,m[t],e)},delegateYield:function(t,o,s){return e.resultName=o,r(n.d,c(t),s)},finish:function(t){return r(n.f,t)}},r=function(t,r,o){n.p=e.prev,n.n=e.next;try{return t(r,o)}finally{e.next=n.n}}),e.resultName&&(e[e.resultName]=n.v,e.resultName=void 0),e.sent=n.v,e.next=n.n;try{return t.call(this,e)}finally{n.p=e.prev,n.n=e.next}}}return(t.exports=p=function(){return{wrap:function(t,r,n,o){return e.w(d(t),r,n,o&&o.reverse())},isGeneratorFunction:f,mark:e.m,awrap:function(t,e){return new n(t,e)},AsyncIterator:a,async:function(t,e,r,n,o){return(f(e)?u:s)(d(t),e,r,n,o)},keys:i,values:c}},t.exports.__esModule=!0,t.exports.default=t.exports)()}t.exports=p,t.exports.__esModule=!0,t.exports.default=t.exports},760(t,e,r){var n=r(735).default;t.exports=function(t){if(null!=t){var e=t["function"==typeof Symbol&&Symbol.iterator||"@@iterator"],r=0;if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length))return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}}}throw new TypeError(n(t)+" is not iterable")},t.exports.__esModule=!0,t.exports.default=t.exports},735(t){function e(r){return t.exports=e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t.exports.__esModule=!0,t.exports.default=t.exports,e(r)}t.exports=e,t.exports.__esModule=!0,t.exports.default=t.exports},183(t,e,r){var n=r(24)();t.exports=n;try{regeneratorRuntime=n}catch(t){"object"==typeof globalThis?globalThis.regeneratorRuntime=n:Function("r","regeneratorRuntime = r")(n)}}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var s=e[n]={exports:{}};return t[n](s,s.exports,r),s.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";const t=flarum.core.compat["forum/app"];var e=r.n(t);const n=flarum.core.compat["forum/components/Search"];var o=r.n(n);const s=flarum.core.compat["common/extend"];function u(t,e,r,n,o,s,u){try{var a=t[s](u),i=a.value}catch(t){return void r(t)}a.done?e(i):Promise.resolve(i).then(n,o)}function a(t){return function(){var e=this,r=arguments;return new Promise(function(n,o){var s=t.apply(e,r);function a(t){u(s,n,o,a,i,"next",t)}function i(t){u(s,n,o,a,i,"throw",t)}a(void 0)})}}var i=r(183),c=r.n(i);const p=flarum.core.compat["common/helpers/highlight"];var l=r.n(p);const f=flarum.core.compat["common/components/LinkButton"];var d=r.n(f);const v=flarum.core.compat["common/components/Link"];var h=r.n(v),x=function(){function t(){this.results=new Map,this.type="discussions"}var r=t.prototype;return r.search=function(){var t=a(c().mark(function t(r){var n,o,s,u;return c().wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return r=r.toLowerCase(),this.results.set(r,[]),n={filter:{q:r,autocomplete:1},page:{limit:3},include:"mostRelevantPost"},o=e().forum.attribute("apiUrl")+"/sbp/search/"+this.type,t.next=6,e().request({params:n,url:o,method:"GET"});case 6:s=t.sent,u=e().store.pushPayload(s),this.results.set(r,u);case 9:case"end":return t.stop()}},t,this)}));return function(e){return t.apply(this,arguments)}}(),r.view=function(t){var r=this;t=t.toLowerCase();var n=(this.results.get(t)||[]).map(function(n){var o=n.mostRelevantPost();return m("li",{className:"DiscussionSearchResult","data-index":""+r.type+n.id()},m(h(),{href:e().route.discussion(n,o&&o.number())},m("div",{className:"DiscussionSearchResult-title"},l()(n.title(),t)),!!o&&m("div",{className:"DiscussionSearchResult-excerpt"},l()(o.contentPlain(),t,100))))});return[m("li",{className:"Dropdown-header"},e().translator.trans("core.forum.search.discussions_heading")),m("li",null,m(d(),{icon:"fas fa-search",href:e().route("index",{q:t})},e().translator.trans("core.forum.search.all_discussions_button",{query:t})))].concat(n)},t}();function y(){return y=Object.assign?Object.assign.bind():function(t){for(var e=1;e 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0));\n }\n if (o || r > 1) return a;\n throw y = !0, n;\n }\n return function (o, p, l) {\n if (f > 1) throw TypeError(\"Generator is already running\");\n for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) {\n i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u);\n try {\n if (f = 2, i) {\n if (c || (o = \"next\"), t = i[o]) {\n if (!(t = t.call(i, u))) throw TypeError(\"iterator result is not an object\");\n if (!t.done) return t;\n u = t.value, c < 2 && (c = 0);\n } else 1 === c && (t = i[\"return\"]) && t.call(i), c < 2 && (u = TypeError(\"The iterator does not provide a '\" + o + \"' method\"), c = 1);\n i = e;\n } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break;\n } catch (t) {\n i = e, c = 1, u = t;\n } finally {\n f = 1;\n }\n }\n return {\n value: t,\n done: y\n };\n };\n }(r, o, i), !0), u;\n }\n var a = {};\n function Generator() {}\n function GeneratorFunction() {}\n function GeneratorFunctionPrototype() {}\n t = Object.getPrototypeOf;\n var c = [][n] ? t(t([][n]())) : (regeneratorDefine(t = {}, n, function () {\n return this;\n }), t),\n u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c);\n function f(e) {\n return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, regeneratorDefine(e, o, \"GeneratorFunction\")), e.prototype = Object.create(u), e;\n }\n return GeneratorFunction.prototype = GeneratorFunctionPrototype, regeneratorDefine(u, \"constructor\", GeneratorFunctionPrototype), regeneratorDefine(GeneratorFunctionPrototype, \"constructor\", GeneratorFunction), GeneratorFunction.displayName = \"GeneratorFunction\", regeneratorDefine(GeneratorFunctionPrototype, o, \"GeneratorFunction\"), regeneratorDefine(u), regeneratorDefine(u, o, \"Generator\"), regeneratorDefine(u, n, function () {\n return this;\n }), regeneratorDefine(u, \"toString\", function () {\n return \"[object Generator]\";\n }), (module.exports = _regenerator = function _regenerator() {\n return {\n w: i,\n m: f\n };\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports)();\n}\nmodule.exports = _regenerator, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var regeneratorAsyncGen = require(\"./regeneratorAsyncGen.js\");\nfunction _regeneratorAsync(n, e, r, t, o) {\n var a = regeneratorAsyncGen(n, e, r, t, o);\n return a.next().then(function (n) {\n return n.done ? n.value : a.next();\n });\n}\nmodule.exports = _regeneratorAsync, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var regenerator = require(\"./regenerator.js\");\nvar regeneratorAsyncIterator = require(\"./regeneratorAsyncIterator.js\");\nfunction _regeneratorAsyncGen(r, e, t, o, n) {\n return new regeneratorAsyncIterator(regenerator().w(r, e, t, o), n || Promise);\n}\nmodule.exports = _regeneratorAsyncGen, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var OverloadYield = require(\"./OverloadYield.js\");\nvar regeneratorDefine = require(\"./regeneratorDefine.js\");\nfunction AsyncIterator(t, e) {\n function n(r, o, i, f) {\n try {\n var c = t[r](o),\n u = c.value;\n return u instanceof OverloadYield ? e.resolve(u.v).then(function (t) {\n n(\"next\", t, i, f);\n }, function (t) {\n n(\"throw\", t, i, f);\n }) : e.resolve(u).then(function (t) {\n c.value = t, i(c);\n }, function (t) {\n return n(\"throw\", t, i, f);\n });\n } catch (t) {\n f(t);\n }\n }\n var r;\n this.next || (regeneratorDefine(AsyncIterator.prototype), regeneratorDefine(AsyncIterator.prototype, \"function\" == typeof Symbol && Symbol.asyncIterator || \"@asyncIterator\", function () {\n return this;\n })), regeneratorDefine(this, \"_invoke\", function (t, o, i) {\n function f() {\n return new e(function (e, r) {\n n(t, i, e, r);\n });\n }\n return r = r ? r.then(f, f) : f();\n }, !0);\n}\nmodule.exports = AsyncIterator, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _regeneratorDefine(e, r, n, t) {\n var i = Object.defineProperty;\n try {\n i({}, \"\", {});\n } catch (e) {\n i = 0;\n }\n module.exports = _regeneratorDefine = function regeneratorDefine(e, r, n, t) {\n function o(r, n) {\n _regeneratorDefine(e, r, function (e) {\n return this._invoke(r, n, e);\n });\n }\n r ? i ? i(e, r, {\n value: n,\n enumerable: !t,\n configurable: !t,\n writable: !t\n }) : e[r] = n : (o(\"next\", 0), o(\"throw\", 1), o(\"return\", 2));\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports, _regeneratorDefine(e, r, n, t);\n}\nmodule.exports = _regeneratorDefine, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _regeneratorKeys(e) {\n var n = Object(e),\n r = [];\n for (var t in n) r.unshift(t);\n return function e() {\n for (; r.length;) if ((t = r.pop()) in n) return e.value = t, e.done = !1, e;\n return e.done = !0, e;\n };\n}\nmodule.exports = _regeneratorKeys, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var OverloadYield = require(\"./OverloadYield.js\");\nvar regenerator = require(\"./regenerator.js\");\nvar regeneratorAsync = require(\"./regeneratorAsync.js\");\nvar regeneratorAsyncGen = require(\"./regeneratorAsyncGen.js\");\nvar regeneratorAsyncIterator = require(\"./regeneratorAsyncIterator.js\");\nvar regeneratorKeys = require(\"./regeneratorKeys.js\");\nvar regeneratorValues = require(\"./regeneratorValues.js\");\nfunction _regeneratorRuntime() {\n \"use strict\";\n\n var r = regenerator(),\n e = r.m(_regeneratorRuntime),\n t = (Object.getPrototypeOf ? Object.getPrototypeOf(e) : e.__proto__).constructor;\n function n(r) {\n var e = \"function\" == typeof r && r.constructor;\n return !!e && (e === t || \"GeneratorFunction\" === (e.displayName || e.name));\n }\n var o = {\n \"throw\": 1,\n \"return\": 2,\n \"break\": 3,\n \"continue\": 3\n };\n function a(r) {\n var e, t;\n return function (n) {\n e || (e = {\n stop: function stop() {\n return t(n.a, 2);\n },\n \"catch\": function _catch() {\n return n.v;\n },\n abrupt: function abrupt(r, e) {\n return t(n.a, o[r], e);\n },\n delegateYield: function delegateYield(r, o, a) {\n return e.resultName = o, t(n.d, regeneratorValues(r), a);\n },\n finish: function finish(r) {\n return t(n.f, r);\n }\n }, t = function t(r, _t, o) {\n n.p = e.prev, n.n = e.next;\n try {\n return r(_t, o);\n } finally {\n e.next = n.n;\n }\n }), e.resultName && (e[e.resultName] = n.v, e.resultName = void 0), e.sent = n.v, e.next = n.n;\n try {\n return r.call(this, e);\n } finally {\n n.p = e.prev, n.n = e.next;\n }\n };\n }\n return (module.exports = _regeneratorRuntime = function _regeneratorRuntime() {\n return {\n wrap: function wrap(e, t, n, o) {\n return r.w(a(e), t, n, o && o.reverse());\n },\n isGeneratorFunction: n,\n mark: r.m,\n awrap: function awrap(r, e) {\n return new OverloadYield(r, e);\n },\n AsyncIterator: regeneratorAsyncIterator,\n async: function async(r, e, t, o, u) {\n return (n(e) ? regeneratorAsyncGen : regeneratorAsync)(a(r), e, t, o, u);\n },\n keys: regeneratorKeys,\n values: regeneratorValues\n };\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports)();\n}\nmodule.exports = _regeneratorRuntime, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var _typeof = require(\"./typeof.js\")[\"default\"];\nfunction _regeneratorValues(e) {\n if (null != e) {\n var t = e[\"function\" == typeof Symbol && Symbol.iterator || \"@@iterator\"],\n r = 0;\n if (t) return t.call(e);\n if (\"function\" == typeof e.next) return e;\n if (!isNaN(e.length)) return {\n next: function next() {\n return e && r >= e.length && (e = void 0), {\n value: e && e[r++],\n done: !e\n };\n }\n };\n }\n throw new TypeError(_typeof(e) + \" is not iterable\");\n}\nmodule.exports = _regeneratorValues, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return module.exports = _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports, _typeof(o);\n}\nmodule.exports = _typeof, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","// TODO(Babel 8): Remove this file.\n\nvar runtime = require(\"../helpers/regeneratorRuntime\")();\nmodule.exports = runtime;\n\n// Copied from https://github.com/facebook/regenerator/blob/main/packages/runtime/runtime.js#L736=\ntry {\n regeneratorRuntime = runtime;\n} catch (accidentalStrictMode) {\n if (typeof globalThis === \"object\") {\n globalThis.regeneratorRuntime = runtime;\n } else {\n Function(\"r\", \"regeneratorRuntime = r\")(runtime);\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/Search'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","function asyncGeneratorStep(n, t, e, r, o, a, c) {\n try {\n var i = n[a](c),\n u = i.value;\n } catch (n) {\n return void e(n);\n }\n i.done ? t(u) : Promise.resolve(u).then(r, o);\n}\nfunction _asyncToGenerator(n) {\n return function () {\n var t = this,\n e = arguments;\n return new Promise(function (r, o) {\n var a = n.apply(t, e);\n function _next(n) {\n asyncGeneratorStep(a, r, o, _next, _throw, \"next\", n);\n }\n function _throw(n) {\n asyncGeneratorStep(a, r, o, _next, _throw, \"throw\", n);\n }\n _next(void 0);\n });\n };\n}\nexport { _asyncToGenerator as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/highlight'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Link'];","import app from 'flarum/forum/app';\nimport highlight from 'flarum/common/helpers/highlight';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport Link from 'flarum/common/components/Link';\nimport { SearchSource } from 'flarum/forum/components/Search';\nimport type Mithril from 'mithril';\n\n/**\n * The `DiscussionsSearchSource` finds and displays discussion search results in\n * the search dropdown.\n */\nexport default class DiscussionsSearchSource implements SearchSource {\n /**\n * Map of lowercase search queries to their respective model results\n */\n protected results = new Map();\n\n /**\n * Model name used for assembling API endpoint URL, and for frontend DOM.\n */\n private type = 'discussions';\n\n async search(query: string): Promise {\n query = query.toLowerCase();\n\n this.results.set(query, []);\n\n const params = {\n filter: { q: query },\n page: { limit: 3 },\n include: 'mostRelevantPost',\n };\n\n // Construct API search URI\n const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`;\n\n // Make API GET request\n const results = await app.request({ params, url, method: 'GET' });\n\n // Parse API response into models and push to store\n const models = app.store.pushPayload(results);\n\n // Add models to results map\n this.results.set(query, models);\n }\n\n view(query: string): Array {\n query = query.toLowerCase();\n\n // Get results from map\n const queryResults = this.results.get(query) || [];\n\n const results = queryResults.map((discussion: any) => {\n const mostRelevantPost = discussion.mostRelevantPost();\n\n return (\n
  • \n \n
    {highlight(discussion.title(), query)}
    \n {!!mostRelevantPost &&
    {highlight(mostRelevantPost.contentPlain(), query, 100)}
    }\n \n
  • \n );\n });\n\n return [\n
  • {app.translator.trans('core.forum.search.discussions_heading')}
  • ,\n
  • \n \n {app.translator.trans('core.forum.search.all_discussions_button', { query })}\n \n
  • ,\n ...results,\n ];\n }\n}\n","function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/states/DiscussionListState'];","import app from 'flarum/forum/app';\n\nimport Search, { SearchAttrs, SearchSource } from 'flarum/forum/components/Search';\n\nimport { extend } from 'flarum/common/extend';\nimport ItemList from 'flarum/common/utils/ItemList';\n\nimport DiscussionsSearchSource from './SearchSources/DiscussionsSearchSource';\nimport extendDiscussionState from './PaginatedListStates/extendDiscussionState';\n\napp.initializers.add('blomstra-search', () => {\n const minLength = parseInt(app.data.settings['blomstra-search.min-search-length'] || String(Search.MIN_SEARCH_LEN), 10);\n if (minLength !== Search.MIN_SEARCH_LEN) {\n // Flarum provides no extension point for MIN_SEARCH_LEN, so we overwrite the\n // static property directly. TypeScript `readonly` is compile-time only — at\n // runtime this is a plain property assignment and is safe as long as no code\n // reads MIN_SEARCH_LEN before this initializer runs.\n (Search as any).MIN_SEARCH_LEN = minLength;\n }\n\n extend(Search.prototype, 'sourceItems', function (this: Search, items: ItemList) {\n items.replace('discussions', new DiscussionsSearchSource());\n });\n});\n\napp.initializers.add(\n 'blomstra-search-early',\n () => {\n extendDiscussionState();\n },\n 999999\n);\n","import app from 'flarum/forum/app';\n\nimport { override } from 'flarum/common/extend';\n\nimport DiscussionListState from 'flarum/forum/states/DiscussionListState';\n\nexport default function extendDiscussionState() {\n override(DiscussionListState.prototype, 'loadPage', async function (this: DiscussionListState, original, page: number = 1) {\n const preloaded = app.data.apiDocument || null;\n\n // If existing payload is given or no search is made, fallback on native page.\n if (preloaded || !this.requestParams()?.filter?.q) return original.call(this, page);\n\n const params = this.requestParams();\n params.page = {\n offset: this.pageSize * (page - 1),\n ...params.page,\n };\n\n if (Array.isArray(params.include)) {\n params.include = params.include.join(',');\n }\n\n // Construct API search URI\n const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`;\n\n // Make API GET request\n const results = await app.request({ params, url, method: 'GET' });\n\n // Parse API response into models and push to store\n return app.store.pushPayload(results);\n });\n}\n"],"names":["module","exports","e","d","this","v","k","__esModule","regeneratorDefine","require","_regenerator","t","r","Symbol","n","iterator","o","toStringTag","i","c","prototype","Generator","u","Object","create","f","p","y","G","a","bind","length","l","TypeError","call","done","value","GeneratorFunction","GeneratorFunctionPrototype","getPrototypeOf","setPrototypeOf","__proto__","displayName","w","m","regeneratorAsyncGen","next","then","regenerator","regeneratorAsyncIterator","Promise","OverloadYield","AsyncIterator","resolve","asyncIterator","_regeneratorDefine","defineProperty","_invoke","enumerable","configurable","writable","unshift","pop","regeneratorAsync","regeneratorKeys","regeneratorValues","_regeneratorRuntime","constructor","name","stop","abrupt","delegateYield","resultName","finish","_t","prev","sent","wrap","reverse","isGeneratorFunction","mark","awrap","async","keys","values","_typeof","isNaN","runtime","regeneratorRuntime","accidentalStrictMode","globalThis","Function","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","getter","definition","key","get","obj","prop","hasOwnProperty","flarum","core","compat","asyncGeneratorStep","_asyncToGenerator","arguments","apply","_next","_throw","DiscussionsSearchSource","results","Map","type","_proto","search","_search","_callee","query","params","url","models","_context","toLowerCase","set","filter","q","page","limit","include","app","attribute","method","pushPayload","_x","view","_this","map","discussion","mostRelevantPost","className","id","Link","href","number","highlight","title","contentPlain","trans","LinkButton","icon","concat","_extends","assign","add","minLength","parseInt","settings","String","Search","extend","items","replace","override","DiscussionListState","_ref","original","_this$requestParams","_this$requestParams$f","apiDocument","requestParams","offset","pageSize","Array","isArray","join","_x2","_x3"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum.js","mappings":"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","sources":["webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/OverloadYield.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regenerator.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorAsync.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorAsyncGen.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorAsyncIterator.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorDefine.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorKeys.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorRuntime.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/regeneratorValues.js","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/typeof.js","webpack://@blomstra/search/./node_modules/@babel/runtime/regenerator/index.js","webpack://@blomstra/search/webpack/bootstrap","webpack://@blomstra/search/webpack/runtime/compat get default export","webpack://@blomstra/search/webpack/runtime/define property getters","webpack://@blomstra/search/webpack/runtime/hasOwnProperty shorthand","webpack://@blomstra/search/external root \"flarum.core.compat['forum/app']\"","webpack://@blomstra/search/external root \"flarum.core.compat['forum/components/Search']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/extend']\"","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js","webpack://@blomstra/search/external root \"flarum.core.compat['common/helpers/highlight']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/components/LinkButton']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/components/Link']\"","webpack://@blomstra/search/./src/forum/SearchSources/DiscussionsSearchSource.tsx","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/extends.js","webpack://@blomstra/search/external root \"flarum.core.compat['forum/states/DiscussionListState']\"","webpack://@blomstra/search/external root \"flarum.core.compat['forum/components/DiscussionListItem']\"","webpack://@blomstra/search/external root \"flarum.core.compat['forum/components/TerminalPost']\"","webpack://@blomstra/search/./src/forum/index.ts","webpack://@blomstra/search/./src/forum/PaginatedListStates/extendDiscussionState.tsx","webpack://@blomstra/search/./src/forum/extendDiscussionListItem.tsx"],"sourcesContent":["function _OverloadYield(e, d) {\n this.v = e, this.k = d;\n}\nmodule.exports = _OverloadYield, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var regeneratorDefine = require(\"./regeneratorDefine.js\");\nfunction _regenerator() {\n /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */\n var e,\n t,\n r = \"function\" == typeof Symbol ? Symbol : {},\n n = r.iterator || \"@@iterator\",\n o = r.toStringTag || \"@@toStringTag\";\n function i(r, n, o, i) {\n var c = n && n.prototype instanceof Generator ? n : Generator,\n u = Object.create(c.prototype);\n return regeneratorDefine(u, \"_invoke\", function (r, n, o) {\n var i,\n c,\n u,\n f = 0,\n p = o || [],\n y = !1,\n G = {\n p: 0,\n n: 0,\n v: e,\n a: d,\n f: d.bind(e, 4),\n d: function d(t, r) {\n return i = t, c = 0, u = e, G.n = r, a;\n }\n };\n function d(r, n) {\n for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) {\n var o,\n i = p[t],\n d = G.p,\n l = i[2];\n r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0));\n }\n if (o || r > 1) return a;\n throw y = !0, n;\n }\n return function (o, p, l) {\n if (f > 1) throw TypeError(\"Generator is already running\");\n for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) {\n i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u);\n try {\n if (f = 2, i) {\n if (c || (o = \"next\"), t = i[o]) {\n if (!(t = t.call(i, u))) throw TypeError(\"iterator result is not an object\");\n if (!t.done) return t;\n u = t.value, c < 2 && (c = 0);\n } else 1 === c && (t = i[\"return\"]) && t.call(i), c < 2 && (u = TypeError(\"The iterator does not provide a '\" + o + \"' method\"), c = 1);\n i = e;\n } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break;\n } catch (t) {\n i = e, c = 1, u = t;\n } finally {\n f = 1;\n }\n }\n return {\n value: t,\n done: y\n };\n };\n }(r, o, i), !0), u;\n }\n var a = {};\n function Generator() {}\n function GeneratorFunction() {}\n function GeneratorFunctionPrototype() {}\n t = Object.getPrototypeOf;\n var c = [][n] ? t(t([][n]())) : (regeneratorDefine(t = {}, n, function () {\n return this;\n }), t),\n u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c);\n function f(e) {\n return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, regeneratorDefine(e, o, \"GeneratorFunction\")), e.prototype = Object.create(u), e;\n }\n return GeneratorFunction.prototype = GeneratorFunctionPrototype, regeneratorDefine(u, \"constructor\", GeneratorFunctionPrototype), regeneratorDefine(GeneratorFunctionPrototype, \"constructor\", GeneratorFunction), GeneratorFunction.displayName = \"GeneratorFunction\", regeneratorDefine(GeneratorFunctionPrototype, o, \"GeneratorFunction\"), regeneratorDefine(u), regeneratorDefine(u, o, \"Generator\"), regeneratorDefine(u, n, function () {\n return this;\n }), regeneratorDefine(u, \"toString\", function () {\n return \"[object Generator]\";\n }), (module.exports = _regenerator = function _regenerator() {\n return {\n w: i,\n m: f\n };\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports)();\n}\nmodule.exports = _regenerator, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var regeneratorAsyncGen = require(\"./regeneratorAsyncGen.js\");\nfunction _regeneratorAsync(n, e, r, t, o) {\n var a = regeneratorAsyncGen(n, e, r, t, o);\n return a.next().then(function (n) {\n return n.done ? n.value : a.next();\n });\n}\nmodule.exports = _regeneratorAsync, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var regenerator = require(\"./regenerator.js\");\nvar regeneratorAsyncIterator = require(\"./regeneratorAsyncIterator.js\");\nfunction _regeneratorAsyncGen(r, e, t, o, n) {\n return new regeneratorAsyncIterator(regenerator().w(r, e, t, o), n || Promise);\n}\nmodule.exports = _regeneratorAsyncGen, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var OverloadYield = require(\"./OverloadYield.js\");\nvar regeneratorDefine = require(\"./regeneratorDefine.js\");\nfunction AsyncIterator(t, e) {\n function n(r, o, i, f) {\n try {\n var c = t[r](o),\n u = c.value;\n return u instanceof OverloadYield ? e.resolve(u.v).then(function (t) {\n n(\"next\", t, i, f);\n }, function (t) {\n n(\"throw\", t, i, f);\n }) : e.resolve(u).then(function (t) {\n c.value = t, i(c);\n }, function (t) {\n return n(\"throw\", t, i, f);\n });\n } catch (t) {\n f(t);\n }\n }\n var r;\n this.next || (regeneratorDefine(AsyncIterator.prototype), regeneratorDefine(AsyncIterator.prototype, \"function\" == typeof Symbol && Symbol.asyncIterator || \"@asyncIterator\", function () {\n return this;\n })), regeneratorDefine(this, \"_invoke\", function (t, o, i) {\n function f() {\n return new e(function (e, r) {\n n(t, i, e, r);\n });\n }\n return r = r ? r.then(f, f) : f();\n }, !0);\n}\nmodule.exports = AsyncIterator, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _regeneratorDefine(e, r, n, t) {\n var i = Object.defineProperty;\n try {\n i({}, \"\", {});\n } catch (e) {\n i = 0;\n }\n module.exports = _regeneratorDefine = function regeneratorDefine(e, r, n, t) {\n function o(r, n) {\n _regeneratorDefine(e, r, function (e) {\n return this._invoke(r, n, e);\n });\n }\n r ? i ? i(e, r, {\n value: n,\n enumerable: !t,\n configurable: !t,\n writable: !t\n }) : e[r] = n : (o(\"next\", 0), o(\"throw\", 1), o(\"return\", 2));\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports, _regeneratorDefine(e, r, n, t);\n}\nmodule.exports = _regeneratorDefine, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _regeneratorKeys(e) {\n var n = Object(e),\n r = [];\n for (var t in n) r.unshift(t);\n return function e() {\n for (; r.length;) if ((t = r.pop()) in n) return e.value = t, e.done = !1, e;\n return e.done = !0, e;\n };\n}\nmodule.exports = _regeneratorKeys, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var OverloadYield = require(\"./OverloadYield.js\");\nvar regenerator = require(\"./regenerator.js\");\nvar regeneratorAsync = require(\"./regeneratorAsync.js\");\nvar regeneratorAsyncGen = require(\"./regeneratorAsyncGen.js\");\nvar regeneratorAsyncIterator = require(\"./regeneratorAsyncIterator.js\");\nvar regeneratorKeys = require(\"./regeneratorKeys.js\");\nvar regeneratorValues = require(\"./regeneratorValues.js\");\nfunction _regeneratorRuntime() {\n \"use strict\";\n\n var r = regenerator(),\n e = r.m(_regeneratorRuntime),\n t = (Object.getPrototypeOf ? Object.getPrototypeOf(e) : e.__proto__).constructor;\n function n(r) {\n var e = \"function\" == typeof r && r.constructor;\n return !!e && (e === t || \"GeneratorFunction\" === (e.displayName || e.name));\n }\n var o = {\n \"throw\": 1,\n \"return\": 2,\n \"break\": 3,\n \"continue\": 3\n };\n function a(r) {\n var e, t;\n return function (n) {\n e || (e = {\n stop: function stop() {\n return t(n.a, 2);\n },\n \"catch\": function _catch() {\n return n.v;\n },\n abrupt: function abrupt(r, e) {\n return t(n.a, o[r], e);\n },\n delegateYield: function delegateYield(r, o, a) {\n return e.resultName = o, t(n.d, regeneratorValues(r), a);\n },\n finish: function finish(r) {\n return t(n.f, r);\n }\n }, t = function t(r, _t, o) {\n n.p = e.prev, n.n = e.next;\n try {\n return r(_t, o);\n } finally {\n e.next = n.n;\n }\n }), e.resultName && (e[e.resultName] = n.v, e.resultName = void 0), e.sent = n.v, e.next = n.n;\n try {\n return r.call(this, e);\n } finally {\n n.p = e.prev, n.n = e.next;\n }\n };\n }\n return (module.exports = _regeneratorRuntime = function _regeneratorRuntime() {\n return {\n wrap: function wrap(e, t, n, o) {\n return r.w(a(e), t, n, o && o.reverse());\n },\n isGeneratorFunction: n,\n mark: r.m,\n awrap: function awrap(r, e) {\n return new OverloadYield(r, e);\n },\n AsyncIterator: regeneratorAsyncIterator,\n async: function async(r, e, t, o, u) {\n return (n(e) ? regeneratorAsyncGen : regeneratorAsync)(a(r), e, t, o, u);\n },\n keys: regeneratorKeys,\n values: regeneratorValues\n };\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports)();\n}\nmodule.exports = _regeneratorRuntime, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var _typeof = require(\"./typeof.js\")[\"default\"];\nfunction _regeneratorValues(e) {\n if (null != e) {\n var t = e[\"function\" == typeof Symbol && Symbol.iterator || \"@@iterator\"],\n r = 0;\n if (t) return t.call(e);\n if (\"function\" == typeof e.next) return e;\n if (!isNaN(e.length)) return {\n next: function next() {\n return e && r >= e.length && (e = void 0), {\n value: e && e[r++],\n done: !e\n };\n }\n };\n }\n throw new TypeError(_typeof(e) + \" is not iterable\");\n}\nmodule.exports = _regeneratorValues, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return module.exports = _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports, _typeof(o);\n}\nmodule.exports = _typeof, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","// TODO(Babel 8): Remove this file.\n\nvar runtime = require(\"../helpers/regeneratorRuntime\")();\nmodule.exports = runtime;\n\n// Copied from https://github.com/facebook/regenerator/blob/main/packages/runtime/runtime.js#L736=\ntry {\n regeneratorRuntime = runtime;\n} catch (accidentalStrictMode) {\n if (typeof globalThis === \"object\") {\n globalThis.regeneratorRuntime = runtime;\n } else {\n Function(\"r\", \"regeneratorRuntime = r\")(runtime);\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/Search'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","function asyncGeneratorStep(n, t, e, r, o, a, c) {\n try {\n var i = n[a](c),\n u = i.value;\n } catch (n) {\n return void e(n);\n }\n i.done ? t(u) : Promise.resolve(u).then(r, o);\n}\nfunction _asyncToGenerator(n) {\n return function () {\n var t = this,\n e = arguments;\n return new Promise(function (r, o) {\n var a = n.apply(t, e);\n function _next(n) {\n asyncGeneratorStep(a, r, o, _next, _throw, \"next\", n);\n }\n function _throw(n) {\n asyncGeneratorStep(a, r, o, _next, _throw, \"throw\", n);\n }\n _next(void 0);\n });\n };\n}\nexport { _asyncToGenerator as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/highlight'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Link'];","import app from 'flarum/forum/app';\nimport highlight from 'flarum/common/helpers/highlight';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport Link from 'flarum/common/components/Link';\nimport { SearchSource } from 'flarum/forum/components/Search';\nimport type Mithril from 'mithril';\n\n/**\n * The `DiscussionsSearchSource` finds and displays discussion search results in\n * the search dropdown.\n */\nexport default class DiscussionsSearchSource implements SearchSource {\n /**\n * Map of lowercase search queries to their respective model results\n */\n protected results = new Map();\n\n /**\n * Model name used for assembling API endpoint URL, and for frontend DOM.\n */\n private type = 'discussions';\n\n async search(query: string): Promise {\n // Suppress ES instant results when \"search within comments\" is ON;\n // only usernames are shown in that mode.\n\n query = query.toLowerCase();\n\n this.results.set(query, []);\n\n const params = {\n filter: { q: query, autocomplete: 1 },\n page: { limit: 3 },\n include: 'mostRelevantPost',\n };\n\n // Construct API search URI\n const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`;\n\n // Make API GET request\n const results = await app.request({ params, url, method: 'GET' });\n\n // Parse API response into models and push to store\n const models = app.store.pushPayload(results);\n\n // Add models to results map\n this.results.set(query, models);\n }\n\n view(query: string): Array {\n query = query.toLowerCase();\n\n // Get results from map\n const queryResults = this.results.get(query) || [];\n\n const results = queryResults.map((discussion: any) => {\n const mostRelevantPost = discussion.mostRelevantPost();\n\n return (\n
  • \n \n
    {highlight(discussion.title(), query)}
    \n {!!mostRelevantPost &&
    {highlight(mostRelevantPost.contentPlain(), query, 100)}
    }\n \n
  • \n );\n });\n\n return [\n
  • {app.translator.trans('core.forum.search.discussions_heading')}
  • ,\n
  • \n \n {app.translator.trans('core.forum.search.all_discussions_button', { query })}\n \n
  • ,\n ...results,\n ];\n }\n}\n","function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/states/DiscussionListState'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/DiscussionListItem'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/TerminalPost'];","import app from 'flarum/forum/app';\n\nimport Search, { SearchAttrs, SearchSource } from 'flarum/forum/components/Search';\n\nimport { extend } from 'flarum/common/extend';\nimport ItemList from 'flarum/common/utils/ItemList';\n\nimport DiscussionsSearchSource from './SearchSources/DiscussionsSearchSource';\nimport extendDiscussionState from './PaginatedListStates/extendDiscussionState';\nimport extendDiscussionListItem from './extendDiscussionListItem';\n\napp.initializers.add('sbp-search', () => {\n extend(Search.prototype, 'sourceItems', function (this: Search, items: ItemList) {\n // app.forum is not available during initializers (it is set after they run),\n // so read the setting lazily here, at first render time.\n const minLength = (app.forum.attribute('blomstraSearchMinLength') as number) || Search.MIN_SEARCH_LEN;\n if (minLength !== Search.MIN_SEARCH_LEN) {\n (Search as any).MIN_SEARCH_LEN = minLength;\n }\n\n items.replace('discussions', new DiscussionsSearchSource());\n });\n});\n\napp.initializers.add(\n 'sbp-search-early',\n () => {\n extendDiscussionState();\n extendDiscussionListItem();\n },\n 999999\n);\n","import app from 'flarum/forum/app';\n\nimport { override } from 'flarum/common/extend';\n\nimport DiscussionListState from 'flarum/forum/states/DiscussionListState';\n\nexport default function extendDiscussionState() {\n override(DiscussionListState.prototype, 'loadPage', async function (this: DiscussionListState, original, page: number = 1) {\n\n // If existing payload is given or no search is made, fallback on native page.\n if (!this.requestParams()?.filter?.q) return original.call(this, page);\n\n const params = this.requestParams();\n params.page = {\n offset: this.pageSize * (page - 1),\n ...params.page,\n };\n\n\n if (Array.isArray(params.include)) {\n params.include = params.include.join(',');\n }\n\n // Construct API search URI\n const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`;\n\n // Make API GET request\n const results = await app.request({ params, url, method: 'GET' });\n\n // Parse API response into models and push to store\n return app.store.pushPayload(results);\n });\n}\n","import { extend } from 'flarum/common/extend';\nimport DiscussionListItem from 'flarum/forum/components/DiscussionListItem';\nimport TerminalPost from 'flarum/forum/components/TerminalPost';\n\nexport default function extendDiscussionListItem() {\n extend(DiscussionListItem.prototype, 'infoItems', function (items) {\n const params = this.attrs.params;\n\n if (!params.q) return;\n\n const hasFieldSort = params.sort && params.sort !== 'relevance';\n\n if (hasFieldSort) {\n // Field sort active (latest, oldest, top, …): replace excerpt with TerminalPost.\n // An excerpt is meaningless when results are ordered by date/count rather than relevance.\n items.remove('excerpt');\n if (!items.has('terminalPost')) {\n items.add('terminalPost', );\n }\n } else if (!items.has('excerpt')) {\n // Relevance mode but no excerpt (mostRelevantPost was null or non-comment type).\n // Fall back to TerminalPost so the info section is never silently empty.\n items.add('terminalPost', );\n }\n });\n}\n"],"names":["module","exports","e","d","this","v","k","__esModule","regeneratorDefine","require","_regenerator","t","r","Symbol","n","iterator","o","toStringTag","i","c","prototype","Generator","u","Object","create","f","p","y","G","a","bind","length","l","TypeError","call","done","value","GeneratorFunction","GeneratorFunctionPrototype","getPrototypeOf","setPrototypeOf","__proto__","displayName","w","m","regeneratorAsyncGen","next","then","regenerator","regeneratorAsyncIterator","Promise","OverloadYield","AsyncIterator","resolve","asyncIterator","_regeneratorDefine","defineProperty","_invoke","enumerable","configurable","writable","unshift","pop","regeneratorAsync","regeneratorKeys","regeneratorValues","_regeneratorRuntime","constructor","name","stop","abrupt","delegateYield","resultName","finish","_t","prev","sent","wrap","reverse","isGeneratorFunction","mark","awrap","async","keys","values","_typeof","isNaN","runtime","regeneratorRuntime","accidentalStrictMode","globalThis","Function","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","getter","definition","key","get","obj","prop","hasOwnProperty","flarum","core","compat","asyncGeneratorStep","_asyncToGenerator","arguments","apply","_next","_throw","DiscussionsSearchSource","results","Map","type","_proto","search","_search","_callee","query","params","url","models","_context","toLowerCase","set","filter","q","autocomplete","page","limit","include","app","attribute","method","pushPayload","_x","view","_this","map","discussion","mostRelevantPost","className","id","Link","href","number","highlight","title","contentPlain","trans","LinkButton","icon","concat","_extends","assign","add","extend","Search","items","minLength","replace","override","DiscussionListState","_ref","original","_this$requestParams","_this$requestParams$f","requestParams","offset","pageSize","Array","isArray","join","_x2","_x3","DiscussionListItem","attrs","sort","remove","has","TerminalPost","lastPost","showFirstPost"],"sourceRoot":""} \ No newline at end of file diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts index ff0f0f9..8a5cb94 100644 --- a/js/src/admin/index.ts +++ b/js/src/admin/index.ts @@ -4,7 +4,7 @@ import DashboardPage from 'flarum/admin/components/DashboardPage'; import DashboardWidget from 'flarum/admin/components/DashboardWidget'; import Alert from 'flarum/common/components/Alert'; -const REQUIRED_INDEX_COMPAT = 'v2'; +const REQUIRED_INDEX_COMPAT = 'v3'; class ReindexWarningWidget extends DashboardWidget { className() { @@ -25,7 +25,7 @@ class ReindexWarningWidget extends DashboardWidget { } } -app.initializers.add('blomstra-search', () => { +app.initializers.add('sbp-search', () => { const activeIndex = app.data.settings['blomstra-search.active-index']; const compatVersion = app.data.settings['blomstra-search.index-compatible']; @@ -35,46 +35,44 @@ app.initializers.add('blomstra-search', () => { }); } - const languages = new Map(); - [ - 'arabic', - 'armenian', - 'basque', - 'bengali', - 'brazilian', - 'bulgarian', - 'catalan', - 'cjk', - 'czech', - 'danish', - 'dutch', - 'english', - 'estonian', - 'finnish', - 'french', - 'galician', - 'german', - 'greek', - 'hindi', - 'hungarian', - 'indonesian', - 'irish', - 'italian', - 'latvian', - 'lithuanian', - 'norwegian', - 'persian', - 'portuguese', - 'romanian', - 'russian', - 'sorani', - 'spanish', - 'swedish', - 'turkish', - 'thai', - ].forEach((language) => { - languages.set(language, language); - }); + const languages: Record = { + arabic: 'Arabic', + armenian: 'Armenian', + basque: 'Basque', + bengali: 'Bengali', + brazilian: 'Brazilian Portuguese', + bulgarian: 'Bulgarian', + catalan: 'Catalan', + cjk: 'CJK (Chinese, Japanese, Korean)', + czech: 'Czech', + danish: 'Danish', + dutch: 'Dutch', + english: 'English', + estonian: 'Estonian', + finnish: 'Finnish', + french: 'French', + galician: 'Galician', + german: 'German', + greek: 'Greek', + hindi: 'Hindi', + hungarian: 'Hungarian', + indonesian: 'Indonesian', + irish: 'Irish', + italian: 'Italian', + latvian: 'Latvian', + lithuanian: 'Lithuanian', + norwegian: 'Norwegian', + persian: 'Persian', + portuguese: 'Portuguese', + romanian: 'Romanian', + russian: 'Russian', + sorani: 'Sorani (Kurdish)', + spanish: 'Spanish', + standard: 'Standard (generic)', + swedish: 'Swedish', + turkish: 'Turkish', + thai: 'Thai', + }; app.extensionData .for('blomstra-search') @@ -84,12 +82,10 @@ app.initializers.add('blomstra-search', () => { if (!activeIndex || !indexedAnalyzer) return null; const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english'; - const currentMinLength = this.setting('blomstra-search.min-search-length')(); - const indexedMinLength = String( - app.data.settings['blomstra-search.indexed-min-search-length'] || app.data.settings['blomstra-search.min-search-length'] - ); + const currentStemExclusion = this.setting('blomstra-search.stem-exclusion')() || ''; + const indexedStemExclusion = app.data.settings['blomstra-search.indexed-stem-exclusion'] || ''; - if (currentAnalyzer === indexedAnalyzer && currentMinLength === indexedMinLength) return null; + if (currentAnalyzer === indexedAnalyzer && currentStemExclusion === indexedStemExclusion) return null; return m( Alert, @@ -118,14 +114,6 @@ app.initializers.add('blomstra-search', () => { default: 'flarum', type: 'input', }) - .registerSetting({ - setting: 'blomstra-search.analyzer-language', - label: app.translator.trans('blomstra-search.admin.analyzer.label'), - help: app.translator.trans('blomstra-search.admin.analyzer.help'), - type: 'select', - options: Object.fromEntries(languages.entries()), - default: 'english', - }) .registerSetting({ setting: 'blomstra-search.search-discussion-subjects', label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'), @@ -137,11 +125,33 @@ app.initializers.add('blomstra-search', () => { type: 'switch', }) .registerSetting({ - setting: 'blomstra-search.min-search-length', - label: app.translator.trans('blomstra-search.admin.min-search-length.label'), - help: app.translator.trans('blomstra-search.admin.min-search-length.help'), + setting: 'blomstra-search.analyzer-language', + label: app.translator.trans('blomstra-search.admin.analyzer.label'), + help: app.translator.trans('blomstra-search.admin.analyzer.help'), type: 'select', - options: { '1': '1', '2': '2', '3': '3', '4': '4' }, - default: app.data.settings['blomstra-search.min-search-length'], + options: languages, + default: 'english', + }) + .registerSetting(function (this: any) { + const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk'; + return this.buildSettingComponent({ + setting: 'blomstra-search.stem-exclusion', + type: 'textarea', + label: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.label'), + help: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.help'), + disabled: isCjk, + }); + }) + .registerSetting(function (this: any) { + const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk'; + return this.buildSettingComponent({ + setting: 'blomstra-search.min-search-length', + label: app.translator.trans('blomstra-search.admin.min-search-length.label'), + help: app.translator.trans('blomstra-search.admin.min-search-length.help'), + type: 'select', + options: { '1': '1', '2': '2', '3': '3', '4': '4' }, + default: app.data.settings['blomstra-search.min-search-length'], + disabled: isCjk, + }); }); }); diff --git a/js/src/forum/PaginatedListStates/extendDiscussionState.tsx b/js/src/forum/PaginatedListStates/extendDiscussionState.tsx index f5b3a8d..f857179 100644 --- a/js/src/forum/PaginatedListStates/extendDiscussionState.tsx +++ b/js/src/forum/PaginatedListStates/extendDiscussionState.tsx @@ -6,10 +6,9 @@ import DiscussionListState from 'flarum/forum/states/DiscussionListState'; export default function extendDiscussionState() { override(DiscussionListState.prototype, 'loadPage', async function (this: DiscussionListState, original, page: number = 1) { - const preloaded = app.data.apiDocument || null; // If existing payload is given or no search is made, fallback on native page. - if (preloaded || !this.requestParams()?.filter?.q) return original.call(this, page); + if (!this.requestParams()?.filter?.q) return original.call(this, page); const params = this.requestParams(); params.page = { @@ -17,12 +16,13 @@ export default function extendDiscussionState() { ...params.page, }; + if (Array.isArray(params.include)) { params.include = params.include.join(','); } // Construct API search URI - const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`; + const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`; // Make API GET request const results = await app.request({ params, url, method: 'GET' }); diff --git a/js/src/forum/SearchSources/DiscussionsSearchSource.tsx b/js/src/forum/SearchSources/DiscussionsSearchSource.tsx index c3aff73..3832397 100644 --- a/js/src/forum/SearchSources/DiscussionsSearchSource.tsx +++ b/js/src/forum/SearchSources/DiscussionsSearchSource.tsx @@ -21,18 +21,21 @@ export default class DiscussionsSearchSource implements SearchSource { private type = 'discussions'; async search(query: string): Promise { + // Suppress ES instant results when "search within comments" is ON; + // only usernames are shown in that mode. + query = query.toLowerCase(); this.results.set(query, []); const params = { - filter: { q: query }, + filter: { q: query, autocomplete: 1 }, page: { limit: 3 }, include: 'mostRelevantPost', }; // Construct API search URI - const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`; + const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`; // Make API GET request const results = await app.request({ params, url, method: 'GET' }); diff --git a/js/src/forum/extendDiscussionListItem.tsx b/js/src/forum/extendDiscussionListItem.tsx new file mode 100644 index 0000000..c788aca --- /dev/null +++ b/js/src/forum/extendDiscussionListItem.tsx @@ -0,0 +1,26 @@ +import { extend } from 'flarum/common/extend'; +import DiscussionListItem from 'flarum/forum/components/DiscussionListItem'; +import TerminalPost from 'flarum/forum/components/TerminalPost'; + +export default function extendDiscussionListItem() { + extend(DiscussionListItem.prototype, 'infoItems', function (items) { + const params = this.attrs.params; + + if (!params.q) return; + + const hasFieldSort = params.sort && params.sort !== 'relevance'; + + if (hasFieldSort) { + // Field sort active (latest, oldest, top, …): replace excerpt with TerminalPost. + // An excerpt is meaningless when results are ordered by date/count rather than relevance. + items.remove('excerpt'); + if (!items.has('terminalPost')) { + items.add('terminalPost', ); + } + } else if (!items.has('excerpt')) { + // Relevance mode but no excerpt (mostRelevantPost was null or non-comment type). + // Fall back to TerminalPost so the info section is never silently empty. + items.add('terminalPost', ); + } + }); +} diff --git a/js/src/forum/index.ts b/js/src/forum/index.ts index e1fa5d3..9a29987 100644 --- a/js/src/forum/index.ts +++ b/js/src/forum/index.ts @@ -7,26 +7,26 @@ import ItemList from 'flarum/common/utils/ItemList'; import DiscussionsSearchSource from './SearchSources/DiscussionsSearchSource'; import extendDiscussionState from './PaginatedListStates/extendDiscussionState'; +import extendDiscussionListItem from './extendDiscussionListItem'; -app.initializers.add('blomstra-search', () => { - const minLength = parseInt(app.data.settings['blomstra-search.min-search-length'] || String(Search.MIN_SEARCH_LEN), 10); - if (minLength !== Search.MIN_SEARCH_LEN) { - // Flarum provides no extension point for MIN_SEARCH_LEN, so we overwrite the - // static property directly. TypeScript `readonly` is compile-time only — at - // runtime this is a plain property assignment and is safe as long as no code - // reads MIN_SEARCH_LEN before this initializer runs. - (Search as any).MIN_SEARCH_LEN = minLength; - } - +app.initializers.add('sbp-search', () => { extend(Search.prototype, 'sourceItems', function (this: Search, items: ItemList) { + // app.forum is not available during initializers (it is set after they run), + // so read the setting lazily here, at first render time. + const minLength = (app.forum.attribute('blomstraSearchMinLength') as number) || Search.MIN_SEARCH_LEN; + if (minLength !== Search.MIN_SEARCH_LEN) { + (Search as any).MIN_SEARCH_LEN = minLength; + } + items.replace('discussions', new DiscussionsSearchSource()); }); }); app.initializers.add( - 'blomstra-search-early', + 'sbp-search-early', () => { extendDiscussionState(); + extendDiscussionListItem(); }, 999999 ); diff --git a/patches/0001-title-autocomplete-grams.patch b/patches/0001-title-autocomplete-grams.patch new file mode 100644 index 0000000..ed824da --- /dev/null +++ b/patches/0001-title-autocomplete-grams.patch @@ -0,0 +1,14 @@ +diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php +index 9024e1e..9024e1e 100644 +--- a/src/Commands/BuildCommand.php ++++ b/src/Commands/BuildCommand.php +@@ -38,6 +38,6 @@ class BuildCommand extends Command + /** Matches Flarum's Search::MIN_SEARCH_LEN — the default minimum query length. */ + public const DEFAULT_MIN_SEARCH_LENGTH = 3; + /** N-gram bounds for the title autocomplete subfield (not admin-configurable). */ +- private const TITLE_MIN_GRAM = 2; +- private const TITLE_MAX_GRAM = 15; ++ private const TITLE_MIN_GRAM = 3; ++ private const TITLE_MAX_GRAM = 20; + + protected $signature = 'blomstra:search:index \ No newline at end of file diff --git a/resources/locale/en.yml b/resources/locale/en.yml index ec08d82..5ce7f8e 100644 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -17,9 +17,12 @@ blomstra-search: search-post-bodies: Search inside comments match-sentences: Match search term against full sentence match-words: Match search term against full words + settings: + stem-exclusion: + label: "Protected words (stem exclusion)" + help: "One word per line. These words will not be stemmed during search (e.g. brand names, proper nouns). Not applicable for CJK or Thai languages. Requires a full re-index to take effect." min-search-length: label: Minimum search query length help: | Minimum number of characters required before a search is triggered. Lower this to 1 or 2 for CJK (Chinese, Japanese, Korean) communities where single characters carry full meaning. - Changing this requires a full index rebuild to take effect. diff --git a/src/Api/Client.php b/src/Api/Client.php index 32a914b..3cb522f 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Api; +namespace Sbp\Search\Api; use Illuminate\Support\Arr; use Psr\Http\Message\ResponseInterface; diff --git a/src/Api/Controllers/IndexController.php b/src/Api/Controllers/IndexController.php index b6d2937..6abaa15 100644 --- a/src/Api/Controllers/IndexController.php +++ b/src/Api/Controllers/IndexController.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Api\Controllers; +namespace Sbp\Search\Api\Controllers; -use Blomstra\Search\Commands\BuildCommand; +use Sbp\Search\Commands\BuildCommand; use Laminas\Diactoros\Response\EmptyResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/Api/Controllers/SearchController.php b/src/Api/Controllers/SearchController.php index 32bddb2..abf2bc8 100644 --- a/src/Api/Controllers/SearchController.php +++ b/src/Api/Controllers/SearchController.php @@ -10,15 +10,15 @@ * */ -namespace Blomstra\Search\Api\Controllers; +namespace Sbp\Search\Api\Controllers; -use Blomstra\Search\Elasticsearch\HasChildQuery; -use Blomstra\Search\Elasticsearch\MatchPhraseQuery; -use Blomstra\Search\Elasticsearch\MatchQuery; -use Blomstra\Search\Elasticsearch\TermsQuery; -use Blomstra\Search\Searchers\CommentPostSearcher; -use Blomstra\Search\Searchers\DiscussionSearcher; -use Blomstra\Search\Searchers\Searcher; +use Sbp\Search\Elasticsearch\HasChildQuery; +use Sbp\Search\Elasticsearch\MatchPhraseQuery; +use Sbp\Search\Elasticsearch\MatchQuery; +use Sbp\Search\Elasticsearch\TermsQuery; +use Sbp\Search\Searchers\CommentPostSearcher; +use Sbp\Search\Searchers\DiscussionSearcher; +use Sbp\Search\Searchers\Searcher; use Elasticsearch\Client; use Flarum\Api\Controller\ListDiscussionsController; use Flarum\Api\Serializer\DiscussionSerializer; @@ -37,7 +37,7 @@ use Illuminate\Support\Str; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use Spatie\ElasticsearchQueryBuilder\Builder; -use Spatie\ElasticsearchQueryBuilder\Queries\BoolQuery; +use Sbp\Search\Elasticsearch\BoolQuery; use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; use Spatie\ElasticsearchQueryBuilder\Sorts\Sort; use Tobscure\JsonApi\Document; @@ -78,27 +78,18 @@ class SearchController extends ListDiscussionsController $offset = $this->extractOffset($request); $include = array_merge($this->extractInclude($request), ['state']); - $query = BoolQuery::create() - // Always restrict to discussion documents; posts are only searched via has_child. - ->add(TermQuery::create('join_field', 'discussion'), 'filter'); - - if (!empty($search)) { - $query->add($this->buildTextQuery($search, $actor)); + // Autocomplete path: fast prefix match on title.autocomplete only. + if (!empty($filters['autocomplete']) && !empty($search)) { + return $this->handleAutocomplete($search, $actor, $limit, $offset, $include, $document, $request); } - $this->addFilters($query, $actor, $filters); - - $builder = (new Builder($this->elastic)) - ->index(resolve('blomstra.search.elastic_index')) - ->size($limit + 1) - ->from($offset) - ->addQuery($query); - $knownSortFields = array_merge(array_values($this->translateSort), ['rawId']); $logger = resolve(LoggerInterface::class); $phpSortField = null; $phpSortDir = 'desc'; + $needsScoring = true; + $sorts = []; foreach ($this->extractSort($request) as $field => $direction) { $translated = $this->translateSort[$field] ?? $field; @@ -108,7 +99,8 @@ class SearchController extends ListDiscussionsController continue; } - $builder->addSort(new Sort($translated, $direction)); + $sorts[] = new Sort($translated, $direction); + $needsScoring = false; if ($phpSortField === null && $translated !== 'rawId') { $phpSortField = $translated; @@ -116,7 +108,45 @@ class SearchController extends ListDiscussionsController } } - $response = $builder->search(); + // Default to latest when no explicit sort is requested. This lets has_child use + // score_mode:none, which skips child scoring entirely and lets ES short-circuit + // early on large corpora. Relevance is still available via sort=relevant if added. + if (empty($sorts)) { + $needsScoring = false; + $phpSortField = 'updated_at'; + $phpSortDir = 'desc'; + $sorts[] = new Sort('updated_at', 'desc'); + } + + $query = BoolQuery::create() + // Always restrict to discussion documents; posts are only searched via has_child. + ->add(TermQuery::create('join_field', 'discussion'), 'filter'); + + if (!empty($search)) { + $query->add($this->buildTextQuery($search, $actor, $needsScoring)); + } + + $this->addFilters($query, $actor, $filters); + + $builder = (new Builder($this->elastic)) + ->index(resolve('blomstra.search.elastic_index')) + ->addQuery($query); + + foreach ($sorts as $sort) { + $builder->addSort($sort); + } + + // track_total_hits: false lets ES stop counting once it has collected + // enough results in sort order, avoiding a full-index count on every query. + $payload = $builder->getPayload(); + $payload['track_total_hits'] = false; + + $response = $this->elastic->search([ + 'index' => resolve('blomstra.search.elastic_index'), + 'size' => $limit + 1, + 'from' => $offset, + 'body' => $payload, + ]); Discussion::setStateUser($actor); @@ -192,18 +222,145 @@ class SearchController extends ListDiscussionsController return $discussions; } + /** + * Fast autocomplete path: match prefix n-grams on title.autocomplete only. + * No has_child, no phrase scoring — a single MatchQuery suffices. + */ + protected function handleAutocomplete( + string $search, + User $actor, + int $limit, + int $offset, + array $include, + Document $document, + ServerRequestInterface $request + ): mixed { + $query = BoolQuery::create() + ->add(TermQuery::create('join_field', 'discussion'), 'filter'); + + $query->add($this->buildAutocompleteQuery($search), 'must'); + + $this->addFilters($query, $actor, []); + + $payload = (new Builder($this->elastic)) + ->index(resolve('blomstra.search.elastic_index')) + ->addQuery($query) + ->addSort(new Sort('updated_at', 'desc')) + ->getPayload(); + + $response = $this->elastic->search([ + 'index' => resolve('blomstra.search.elastic_index'), + 'size' => $limit + 1, + 'from' => $offset, + 'body' => $payload, + ]); + + Discussion::setStateUser($actor); + + $results = Collection::make(Arr::get($response, 'hits.hits')) + ->map(fn ($hit) => [ + 'discussion_id' => Str::after($hit['_id'], 'discussions:'), + 'most_relevant_post_id' => null, + 'weight' => Arr::get($hit, 'sort.0', 0), + ]); + + $document->addPaginationLinks( + $this->uri->to('api')->route('blomstra.search', ['type' => 'discussions']), + $request->getQueryParams(), + $offset, + $limit, + $results->count() > $limit ? null : 0 + ); + + $results = $results->take($limit); + + $discussions = Discussion::query() + ->when( + $actor->isGuest() || !$actor->hasPermission('discussion.hide'), + fn ($q) => $q->whereNull('hidden_at') + ) + ->whereIn('id', $results->pluck('discussion_id')->filter()) + ->get() + ->each(function (Discussion $discussion) use ($results) { + $result = $results->firstWhere('discussion_id', $discussion->id); + $discussion->most_relevant_post_id = $result['most_relevant_post_id'] ?? $discussion->first_post_id; + $discussion->weight = $result['weight'] ?? 0; + }) + ->sortByDesc('updated_at') + ->unique(); + + $this->loadRelations($discussions, $include); + + return $discussions; + } + + + protected function buildAutocompleteQuery(string $search): BoolQuery + { + $query = BoolQuery::create(); + + // Title prefix match (stronger) + $query->add( + (new MatchQuery('title.autocomplete', $search)) + ->operator('and') + ->boost(1.0), + 'should' + ); + + // Content prefix match (weaker) + $query->add( + (new MatchQuery('content.autocomplete', $search)) + ->operator('and') + ->boost(0.4), + 'should' + ); + + $query->minimumShouldMatch(1); + + return $query; + } + /** * Build the text-matching portion of the query. * * Discussion titles are matched directly (filtered to join_field=discussion). - * Post bodies are matched via has_child with score_mode=sum so discussions - * with many matching posts score higher than those with a single strong match. - * inner_hits returns the best-scoring post for use as mostRelevantPost. + * Post bodies are matched via has_child. When $needsScoring is true (no explicit + * sort, i.e. relevance ordering), score_mode=sum accumulates child scores onto the + * parent so that discussions with many strongly-matching posts rank higher. When + * false (any explicit field sort), score_mode=none skips scoring entirely — ES only + * checks whether a matching child exists, which is significantly cheaper on large + * corpora. + * inner_hits returns the best-matching post for use as mostRelevantPost. * Hidden posts are only included in matching for users with post.hide permission. */ - protected function buildTextQuery(string $search, User $actor): BoolQuery + protected function buildTextQuery(string $search, User $actor, bool $needsScoring = false): BoolQuery { $textQuery = BoolQuery::create(); + // Strong direct title match for the full search page. + $textQuery->add( + (new MatchQuery('title', $search)) + ->operator('and') + ->boost(4.0), + 'should' + ); + + // Low-weight prefix match on title.autocomplete for partial title hits. + $textQuery->add( + (new MatchQuery('title.autocomplete', $search)) + ->operator('and') + ->boost(0.35), + 'should' + ); + + // Low-weight prefix match on title.autocomplete for the full search page. + // Exact / normal title matches should still rank higher than prefix-only hits. + $textQuery->add( + (new MatchQuery('title.autocomplete', $search)) + ->operator('and') + ->boost(0.35), + 'should' + ); + if ($this->discussionSearcher?->enabled()) { $textQuery->add($this->buildShouldClauses($search, $this->discussionSearcher->boost()), 'should'); @@ -214,11 +371,15 @@ class SearchController extends ListDiscussionsController // Guests and non-moderators may not see hidden posts; exclude them from child matching. if ($actor->isGuest() || !$actor->hasPermission('post.hide')) { - $postQuery->add(TermQuery::create('is_hidden', false), 'filter'); + $postQuery->add(TermQuery::create('is_hidden', 'false'), 'filter'); } + // Without minimum_should_match, ES default MSM is 0 when a filter clause is present, + // causing has_child to score every non-hidden post instead of only matching ones. + $postQuery->minimumShouldMatch(1); + $textQuery->add( - HasChildQuery::create('post', $postQuery)->withInnerHits(), + HasChildQuery::create('post', $postQuery, $needsScoring ? 'sum' : 'none')->withInnerHits(), 'should' ); } @@ -233,10 +394,10 @@ class SearchController extends ListDiscussionsController if ($this->matchSentences) { $should->add((new MatchPhraseQuery('content', $search))->boost(2 * $boost), 'should'); } - if ($this->matchWords) { - $should->add((new MatchQuery('content', $search))->operator('and')->boost(1.8 * $boost), 'should'); - $should->add((new MatchQuery('content', $search))->operator('or')->boost(0.8 * $boost), 'should'); - } + if ($this->matchWords) { + $should->add((new MatchQuery('content', $search))->operator('and')->boost(1.8 * $boost), 'should'); + $should->add((new MatchQuery('content.autocomplete', $search))->operator('and')->boost(0.30 * $boost), 'should'); + } return $should; } @@ -301,8 +462,12 @@ class SearchController extends ListDiscussionsController $search = Arr::get($filters, 'q'); if ($search) { + // Strip Flarum gambit operators (tag:foo, author:bar, is:private, etc.) + // before passing to ES. These are structural filters handled separately; + // leaving them in causes operator:and to require the gambit tokens to + // appear literally in post content, producing zero results. $q = collect(explode(' ', $search)) - ->filter(fn (string $part) => $part !== 'is:private') + ->filter(fn (string $part) => !preg_match('/^\w+:/', $part)) ->filter() ->join(' '); diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 4faf2d7..ef4cb41 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -10,12 +10,13 @@ * */ -namespace Blomstra\Search\Commands; +namespace Sbp\Search\Commands; -use Blomstra\Search\Jobs\Job; -use Blomstra\Search\Jobs\UpdateSearchJob; -use Blomstra\Search\Seeders\Seeder; +use Sbp\Search\Jobs\Job; +use Sbp\Search\Jobs\UpdateSearchJob; +use Sbp\Search\Seeders\Seeder; use Elasticsearch\Client; +use Elasticsearch\Common\Exceptions\ElasticsearchException; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Console\Command; use Illuminate\Contracts\Container\Container; @@ -35,11 +36,15 @@ class BuildCommand extends Command * read back into blomstra-search.index-compatible by saveIndexedConfig so the * value always reflects the live index — including after a promote/rollback. */ - public const INDEX_COMPAT_VERSION = 'v2'; + public const INDEX_COMPAT_VERSION = 'v3'; /** Matches Flarum's Search::MIN_SEARCH_LEN — the default minimum query length. */ public const DEFAULT_MIN_SEARCH_LENGTH = 3; + /** N-gram bounds for the title autocomplete subfield (not admin-configurable). */ + private const TITLE_MIN_GRAM = 3; + private const TITLE_MAX_GRAM = 20; + protected $signature = 'blomstra:search:index {action? : build | promote | rollback | discard | mapping | fill} {--fresh} @@ -154,8 +159,10 @@ HELP; $this->warn("An in-progress build already exists: $staging"); $this->line(''); $this->line('Choose one of:'); - $this->line(' blomstra:search:index build --resume Continue from where it left off'); - $this->line(' blomstra:search:index build --fresh Drop this build and start completely fresh'); + $this->line(' blomstra:search:index build --resume Continue from where it left off'); + $this->line(' blomstra:search:index build --resume --staging Continue and keep in staging when done'); + $this->line(' blomstra:search:index build --fresh Drop this build and start completely fresh'); + $this->line(' blomstra:search:index build --fresh --staging Start fresh and keep in staging when done'); return; } @@ -359,17 +366,7 @@ HELP; $rangeTo = $continueAt; if ($seedMissing) { - $response = (new Builder($client)) - ->index($targetIndex) - ->size(2500) - ->addQuery( - (new BoolQuery()) - ->add((new RangeQuery('rawId'))->gte($rangeFrom)->lte($rangeTo)) - ->add(TermQuery::create('join_field', $seeder->joinRelation())) - ) - ->search(); - - $seeded = Arr::pluck(Arr::get($response, 'hits.hits'), '_source.rawId'); + $seeded = $this->queryIndexedIds($client, $targetIndex, $seeder->joinRelation(), $rangeFrom, $rangeTo); } /** @var Collection $collection */ @@ -498,77 +495,170 @@ HELP; $settingsResponse = $client->indices()->getSettings(['index' => $indexName]); $analysis = Arr::get($settingsResponse, "$indexName.settings.index.analysis", []); - $analyzer = Arr::get($analysis, 'analyzer.flarum_analyzer.type', 'english'); - $minGram = (int) Arr::get($analysis, 'filter.partial_search_filter.min_gram', self::DEFAULT_MIN_SEARCH_LENGTH); + $analyzer = Arr::get($analysis, 'analyzer.flarum_analyzer.type', 'english'); + $stemExclusion = Arr::get($analysis, 'analyzer.flarum_analyzer.stem_exclusion', []); $mappingResponse = $client->indices()->getMapping(['index' => $indexName]); $compatVersion = Arr::get($mappingResponse, "$indexName.mappings._meta.index_compat_version"); $settings->set('blomstra-search.indexed-analyzer', $analyzer); - $settings->set('blomstra-search.indexed-min-search-length', $minGram); + $settings->set('blomstra-search.indexed-stem-exclusion', implode("\n", $stemExclusion)); $settings->set('blomstra-search.index-compatible', $compatVersion); } protected function buildIndexSettings(SettingsRepositoryInterface $settings): array { $language = $settings->get('blomstra-search.analyzer-language') ?: 'english'; - $minGram = max(1, (int) ($settings->get('blomstra-search.min-search-length') ?: self::DEFAULT_MIN_SEARCH_LENGTH)); - $maxGram = 10; - if ($minGram >= $maxGram) { - $this->error("min_gram ($minGram) must be less than max_gram ($maxGram). Using default."); - $minGram = self::DEFAULT_MIN_SEARCH_LENGTH; + $raw = $settings->get('blomstra-search.stem-exclusion', ''); + $stemExclusion = array_values(array_filter(array_map('trim', explode("\n", $raw)))); + + if ($language === 'cjk') { + // CJK uses the built-in bigram analyzer; no autocomplete subfield. + return [ + 'analysis' => [ + 'analyzer' => [ + 'flarum_analyzer' => ['type' => 'cjk'], + ], + ], + ]; + } + + $analyzerConfig = ['type' => $language]; + + if (!empty($stemExclusion)) { + $analyzerConfig['stem_exclusion'] = $stemExclusion; } return [ - 'index.max_ngram_diff' => $maxGram - $minGram, + 'index.max_ngram_diff' => self::TITLE_MAX_GRAM - self::TITLE_MIN_GRAM, 'analysis' => [ - 'analyzer' => [ - 'flarum_analyzer' => [ - 'type' => $language, - ], - 'flarum_analyzer_partial' => [ - 'type' => 'custom', - 'tokenizer' => 'standard', - 'filter' => ['lowercase', 'partial_search_filter'], + 'filter' => [ + 'title_autocomplete_filter' => [ + 'type' => 'edge_ngram', + 'min_gram' => self::TITLE_MIN_GRAM, + 'max_gram' => self::TITLE_MAX_GRAM, + 'token_chars' => ['letter', 'digit'], ], ], - 'filter' => [ - 'partial_search_filter' => [ - 'type' => 'ngram', - 'min_gram' => $minGram, - 'max_gram' => $maxGram, - 'token_chars' => ['letter', 'digit', 'symbol'], + 'analyzer' => [ + // Symmetric language analyzer: used for BOTH index and search on content + title. + 'flarum_analyzer' => $analyzerConfig, + // Title autocomplete index-time: prefix n-grams, no stemming. + 'flarum_title_autocomplete' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => ['lowercase', 'title_autocomplete_filter'], + ], + // Title autocomplete search-time: just lowercase — predictable on partial words. + 'flarum_title_search' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => ['lowercase'], ], ], ], ]; } - protected function mappingProperties(): array - { - return [ - '_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION], - 'properties' => [ - 'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']], - 'discussion_id' => ['type' => 'integer'], - 'content' => ['type' => 'text', 'analyzer' => 'flarum_analyzer_partial', 'search_analyzer' => 'flarum_analyzer'], - 'rawId' => ['type' => 'integer'], - 'created_at' => ['type' => 'date'], - 'updated_at' => ['type' => 'date'], - 'is_private' => ['type' => 'boolean'], - 'is_sticky' => ['type' => 'boolean'], - 'groups' => ['type' => 'integer'], - 'tags' => ['type' => 'integer'], - 'recipient_groups' => ['type' => 'integer'], - 'recipient_users' => ['type' => 'integer'], - 'comment_count' => ['type' => 'integer'], - 'view_count' => ['type' => 'integer'], - 'is_hidden' => ['type' => 'boolean'], +protected function mappingProperties(): array +{ + $language = resolve(SettingsRepositoryInterface::class)->get('blomstra-search.analyzer-language') ?: 'english'; + + $titleMapping = [ + 'type' => 'text', + 'analyzer' => 'flarum_analyzer', + ]; + + // Only add the autocomplete subfield for non-CJK languages. + if ($language !== 'cjk') { + $titleMapping['fields'] = [ + 'autocomplete' => [ + 'type' => 'text', + 'analyzer' => 'flarum_title_autocomplete', + 'search_analyzer' => 'flarum_title_search', ], ]; } + $contentMapping = [ + 'type' => 'text', + 'analyzer' => 'flarum_analyzer', + ]; + + // Only add the autocomplete subfield for non-CJK languages. + if ($language !== 'cjk') { + $contentMapping['fields'] = [ + 'autocomplete' => [ + 'type' => 'text', + 'analyzer' => 'flarum_title_autocomplete', + 'search_analyzer' => 'flarum_title_search', + ], + ]; + } + + return [ + '_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION], + 'properties' => [ + 'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']], + 'discussion_id' => ['type' => 'integer'], + 'content' => $contentMapping, + 'title' => $titleMapping, + 'rawId' => ['type' => 'integer'], + 'created_at' => ['type' => 'date'], + 'updated_at' => ['type' => 'date'], + 'is_private' => ['type' => 'boolean'], + 'user_id' => ['type' => 'integer'], + 'groups' => ['type' => 'keyword'], + 'comment_count' => ['type' => 'integer'], + 'tags' => ['type' => 'keyword'], + 'is_sticky' => ['type' => 'boolean'], + 'is_hidden' => ['type' => 'boolean'], + ], + ]; +} + /** + * Query ES for rawIds already indexed in $targetIndex for the given $joinRelation and ID range. + * Retries on transient ES failures (NoNodesAvailableException / other ElasticsearchException) + * by sleeping past the StaticNoPingConnectionPool dead-node timeout (default 60 s) before + * each retry, giving the pool a chance to resurface the node. + */ + protected function queryIndexedIds( + Client $client, + string $targetIndex, + string $joinRelation, + int $rangeFrom, + int $rangeTo, + int $maxRetries = 10 + ): array { + $attempt = 0; + + while (true) { + try { + $response = (new Builder($client)) + ->index($targetIndex) + ->size(2500) + ->addQuery( + (new BoolQuery()) + ->add((new RangeQuery('rawId'))->gte($rangeFrom)->lte($rangeTo)) + ->add(TermQuery::create('join_field', $joinRelation)) + ) + ->search(); + + return Arr::pluck(Arr::get($response, 'hits.hits'), '_source.rawId'); + } catch (ElasticsearchException $e) { + $attempt++; + + if ($attempt >= $maxRetries) { + throw $e; + } + + $this->warn("ES error on range {$rangeFrom}–{$rangeTo} (attempt {$attempt}/{$maxRetries}): {$e->getMessage()}. Waiting 65 s before retry…"); + sleep(65); // outlast the StaticNoPingConnectionPool dead-node window (default 60 s) + } + } + } + protected function getContinueAt(SettingsRepositoryInterface $settings, string $type): ?int { $raw = $settings->get("blomstra-search.continued-at.$type"); diff --git a/src/Elasticsearch/BoolQuery.php b/src/Elasticsearch/BoolQuery.php new file mode 100644 index 0000000..dc78651 --- /dev/null +++ b/src/Elasticsearch/BoolQuery.php @@ -0,0 +1,41 @@ +minimumShouldMatch = $minimum; + + return $this; + } + + public function toArray(): array + { + $array = parent::toArray(); + + if ($this->minimumShouldMatch !== null) { + $array['bool']['minimum_should_match'] = $this->minimumShouldMatch; + } + + return $array; + } +} diff --git a/src/Elasticsearch/HasChildQuery.php b/src/Elasticsearch/HasChildQuery.php index a95fe0a..3261458 100644 --- a/src/Elasticsearch/HasChildQuery.php +++ b/src/Elasticsearch/HasChildQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; use Spatie\ElasticsearchQueryBuilder\Queries\Query; diff --git a/src/Elasticsearch/MatchPhraseQuery.php b/src/Elasticsearch/MatchPhraseQuery.php index 2c66a00..61a11d8 100644 --- a/src/Elasticsearch/MatchPhraseQuery.php +++ b/src/Elasticsearch/MatchPhraseQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery; diff --git a/src/Elasticsearch/MatchQuery.php b/src/Elasticsearch/MatchQuery.php index 2aa833c..083639c 100644 --- a/src/Elasticsearch/MatchQuery.php +++ b/src/Elasticsearch/MatchQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; class MatchQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery { @@ -62,6 +62,10 @@ class MatchQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery $query['match'][$this->field]['boost'] = $this->boost; $query['match'][$this->field]['zero_terms_query'] = $this->zeroTerms ? 'all' : 'none'; + if ($this->analyzer !== null) { + $query['match'][$this->field]['analyzer'] = $this->analyzer; + } + return $query; } } diff --git a/src/Elasticsearch/SimpleSearchQuery.php b/src/Elasticsearch/SimpleSearchQuery.php index 57d1ab9..94f87ee 100644 --- a/src/Elasticsearch/SimpleSearchQuery.php +++ b/src/Elasticsearch/SimpleSearchQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; use Spatie\ElasticsearchQueryBuilder\Queries\Query; diff --git a/src/Elasticsearch/TermsQuery.php b/src/Elasticsearch/TermsQuery.php index 8f91bc2..e1006cd 100644 --- a/src/Elasticsearch/TermsQuery.php +++ b/src/Elasticsearch/TermsQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; use Spatie\ElasticsearchQueryBuilder\Queries\Query; diff --git a/src/Elasticsearch/WildcardQuery.php b/src/Elasticsearch/WildcardQuery.php index b9ac777..0af8bde 100644 --- a/src/Elasticsearch/WildcardQuery.php +++ b/src/Elasticsearch/WildcardQuery.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Elasticsearch; +namespace Sbp\Search\Elasticsearch; class WildcardQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\WildcardQuery { diff --git a/src/Exceptions/SeedingException.php b/src/Exceptions/SeedingException.php index d186bf3..bc02907 100644 --- a/src/Exceptions/SeedingException.php +++ b/src/Exceptions/SeedingException.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Exceptions; +namespace Sbp\Search\Exceptions; use Throwable; diff --git a/src/Jobs/DeletingJob.php b/src/Jobs/DeletingJob.php index 415e6be..af321b4 100644 --- a/src/Jobs/DeletingJob.php +++ b/src/Jobs/DeletingJob.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Jobs; +namespace Sbp\Search\Jobs; use Elasticsearch\Client; use Illuminate\Database\Eloquent\Model; diff --git a/src/Jobs/Job.php b/src/Jobs/Job.php index 4e29cdb..291b01b 100644 --- a/src/Jobs/Job.php +++ b/src/Jobs/Job.php @@ -10,11 +10,11 @@ * */ -namespace Blomstra\Search\Jobs; +namespace Sbp\Search\Jobs; -use Blomstra\Search\Seeders\Seeder; +use Sbp\Search\Seeders\Seeder; use Flarum\Queue\AbstractJob; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Collection; abstract class Job extends AbstractJob { diff --git a/src/Jobs/UpdateSearchJob.php b/src/Jobs/UpdateSearchJob.php index 373ccb9..88cb8ef 100644 --- a/src/Jobs/UpdateSearchJob.php +++ b/src/Jobs/UpdateSearchJob.php @@ -10,10 +10,11 @@ * */ -namespace Blomstra\Search\Jobs; +namespace Sbp\Search\Jobs; -use Blomstra\Search\Exceptions\SeedingException; +use Sbp\Search\Exceptions\SeedingException; use Elasticsearch\Client; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; @@ -50,10 +51,11 @@ class UpdateSearchJob extends Job $items = Arr::get($response, 'items'); - $error = Arr::get(Arr::first($items), 'index.error.reason'); + $failed = array_filter($items, fn ($item) => isset($item['index']['error'])); + $error = Arr::get(Arr::first($failed), 'index.error.reason', 'unknown error'); throw new SeedingException( - "Failed to seed: $error", + "Failed to seed: $error (" . count($failed) . '/' . count($items) . ' items failed)', $items ); } diff --git a/src/Jobs/ViewsSearchJob.php b/src/Jobs/ViewsSearchJob.php index e83a6a0..9377cbe 100644 --- a/src/Jobs/ViewsSearchJob.php +++ b/src/Jobs/ViewsSearchJob.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Jobs; +namespace Sbp\Search\Jobs; use Elasticsearch\Client; use Flarum\Api\Serializer\DiscussionSerializer; diff --git a/src/Provider.php b/src/Provider.php index 8b13ee2..ecf459f 100644 --- a/src/Provider.php +++ b/src/Provider.php @@ -10,12 +10,12 @@ * */ -namespace Blomstra\Search; +namespace Sbp\Search; -use Blomstra\Search\Jobs\DeletingJob; -use Blomstra\Search\Jobs\Job; -use Blomstra\Search\Jobs\UpdateSearchJob; -use Blomstra\Search\Jobs\ViewsSearchJob; +use Sbp\Search\Jobs\DeletingJob; +use Sbp\Search\Jobs\Job; +use Sbp\Search\Jobs\UpdateSearchJob; +use Sbp\Search\Jobs\ViewsSearchJob; use Elasticsearch\Client as Elastic; use Elasticsearch\ClientBuilder; use Flarum\Api\Client; @@ -26,7 +26,7 @@ use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\Queue; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Collection; use Laminas\Stratigility\MiddlewarePipe; use Psr\Log\LoggerInterface; @@ -47,7 +47,13 @@ class Provider extends AbstractServiceProvider $this->container->singleton(Elastic::class, function (Container $container) use ($settings, $config) { $builder = ClientBuilder::create() - ->setHosts([$settings->get('blomstra-search.elastic-endpoint')]); + ->setHosts([$settings->get('blomstra-search.elastic-endpoint')]) + ->setConnectionParams([ + 'client' => [ + 'connect_timeout' => 2, // fail fast if ES is unreachable + 'timeout' => 10, // allow time for complex queries + ], + ]); if ($config->inDebugMode()) { $builder->setLogger($container->make(LoggerInterface::class)); diff --git a/src/Save/Document.php b/src/Save/Document.php index 07a7b3f..658a2a7 100644 --- a/src/Save/Document.php +++ b/src/Save/Document.php @@ -10,7 +10,7 @@ * */ -namespace Blomstra\Search\Save; +namespace Sbp\Search\Save; use Carbon\Carbon; use Illuminate\Support\Arr; diff --git a/src/Searchers/CommentPostSearcher.php b/src/Searchers/CommentPostSearcher.php index 73bbe78..60acc0d 100644 --- a/src/Searchers/CommentPostSearcher.php +++ b/src/Searchers/CommentPostSearcher.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Searchers; +namespace Sbp\Search\Searchers; -use Blomstra\Search\Seeders\CommentSeeder; +use Sbp\Search\Seeders\CommentSeeder; class CommentPostSearcher extends Searcher { diff --git a/src/Searchers/DiscussionSearcher.php b/src/Searchers/DiscussionSearcher.php index af32452..e959e6e 100644 --- a/src/Searchers/DiscussionSearcher.php +++ b/src/Searchers/DiscussionSearcher.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Searchers; +namespace Sbp\Search\Searchers; -use Blomstra\Search\Seeders\DiscussionSeeder; +use Sbp\Search\Seeders\DiscussionSeeder; class DiscussionSearcher extends Searcher { diff --git a/src/Searchers/Searcher.php b/src/Searchers/Searcher.php index e6b6fe7..adc5693 100644 --- a/src/Searchers/Searcher.php +++ b/src/Searchers/Searcher.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Searchers; +namespace Sbp\Search\Searchers; -use Blomstra\Search\Seeders\Seeder; +use Sbp\Search\Seeders\Seeder; use Flarum\Settings\SettingsRepositoryInterface; abstract class Searcher diff --git a/src/Seeders/CommentSeeder.php b/src/Seeders/CommentSeeder.php index 77d234d..f00c66f 100644 --- a/src/Seeders/CommentSeeder.php +++ b/src/Seeders/CommentSeeder.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Seeders; +namespace Sbp\Search\Seeders; -use Blomstra\Search\Save\Document; +use Sbp\Search\Save\Document; use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\PostSerializer; use Flarum\Discussion\Discussion; diff --git a/src/Seeders/DiscussionSeeder.php b/src/Seeders/DiscussionSeeder.php index 8d60f93..b917ba8 100644 --- a/src/Seeders/DiscussionSeeder.php +++ b/src/Seeders/DiscussionSeeder.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Seeders; +namespace Sbp\Search\Seeders; -use Blomstra\Search\Save\Document; +use Sbp\Search\Save\Document; use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event as Core; @@ -24,7 +24,7 @@ use FoF\Byobu\Events as Byobu; use FoF\DiscussionViews\Events\DiscussionWasViewed; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; class DiscussionSeeder extends Seeder @@ -70,7 +70,7 @@ class DiscussionSeeder extends Seeder { $events->listen([ // flarum/core events - Core\Started::class, Core\Restored::class, + Core\Started::class, Core\Restored::class, Core\Renamed::class, // fof/byobu discussion recipients events. Byobu\DiscussionMadePublic::class, Byobu\RemovedSelf::class, Byobu\RecipientsChanged::class, ], function ($event) use ($callable) { @@ -121,6 +121,7 @@ class DiscussionSeeder extends Seeder 'id' => $this->type().':'.$model->id, 'rawId' => $model->id, 'content' => $model->title, + 'title' => $model->title, 'created_at' => $model->created_at?->toAtomString(), 'updated_at' => $model->last_posted_at?->toAtomString(), 'is_private' => $model->is_private, diff --git a/src/Seeders/Seeder.php b/src/Seeders/Seeder.php index c326750..ffad8ad 100644 --- a/src/Seeders/Seeder.php +++ b/src/Seeders/Seeder.php @@ -10,9 +10,9 @@ * */ -namespace Blomstra\Search\Seeders; +namespace Sbp\Search\Seeders; -use Blomstra\Search\Save\Document; +use Sbp\Search\Save\Document; use Flarum\Extension\ExtensionManager; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Builder;