diff --git a/.gitmodules b/.gitmodules index 62876e832359d456fdcc14776227c4a95b32fce0..037914b8e31c8c9c7a6d84d74657b54524ff3513 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,11 @@ path = extras/packaging/update/sparkle/Sparkle url = https://github.com/sparkle-project/Sparkle.git ignore = dirty +[submodule "3rdparty/md4c"] + path = 3rdparty/md4c + url = https://github.com/mity/md4c.git + ignore = dirty +[submodule "3rdparty/tidy-html5"] + path = 3rdparty/tidy-html5 + url = https://github.com/htacg/tidy-html5.git + ignore = dirty diff --git a/3rdparty/md4c b/3rdparty/md4c new file mode 160000 index 0000000000000000000000000000000000000000..e9ff661ff818ee94a4a231958d9b6768dc6882c9 --- /dev/null +++ b/3rdparty/md4c @@ -0,0 +1 @@ +Subproject commit e9ff661ff818ee94a4a231958d9b6768dc6882c9 diff --git a/3rdparty/tidy-html5 b/3rdparty/tidy-html5 new file mode 160000 index 0000000000000000000000000000000000000000..d08ddc2860aa95ba8e301343a30837f157977cba --- /dev/null +++ b/3rdparty/tidy-html5 @@ -0,0 +1 @@ +Subproject commit d08ddc2860aa95ba8e301343a30837f157977cba diff --git a/CMakeLists.txt b/CMakeLists.txt index e9cac8f50dc6d98c4911f4ca7d9cf4ad7b1b35f1..bf3132e709aa7784af9616868d5fcb57b44fcd29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,18 @@ else() project(jami) endif() +option(WITH_DAEMON_SUBMODULE "Build with daemon submodule" OFF) +option(ENABLE_TESTS "Build with tests" OFF) +option(WITH_WEBENGINE "Build with WebEngine" ON) +if(WITH_WEBENGINE) + add_definitions(-DWITH_WEBENGINE) +endif() + +# init some variables for includes, libs, etc. +set(CLIENT_INCLUDE_DIRS, "") +set(CLIENT_LINK_DIRS, "") +set(CLIENT_LIBS, "") + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb") @@ -41,12 +53,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) # Main project directories: # jami-daemon -if(NOT DEFINED WITH_DAEMON_SUBMODULE) - set(WITH_DAEMON_SUBMODULE false) - # daemon +if(NOT WITH_DAEMON_SUBMODULE) set(DAEMON_DIR ${PROJECT_SOURCE_DIR}/../daemon) else() - # daemon set(DAEMON_DIR ${PROJECT_SOURCE_DIR}/daemon) endif() # src @@ -93,7 +102,6 @@ add_subdirectory(${LIBCLIENT_SRC_DIR}) set(QT_MODULES Quick Network - NetworkAuth Svg Gui Qml @@ -106,10 +114,6 @@ set(QT_MODULES Widgets Positioning) -if(NOT DEFINED WITH_WEBENGINE) - set(WITH_WEBENGINE true) -endif() - if(WITH_WEBENGINE) list(APPEND QT_MODULES WebEngineCore @@ -232,7 +236,9 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/callparticipantsmodel.cpp ${APP_SRC_DIR}/tipsmodel.cpp ${APP_SRC_DIR}/positioning.cpp - ${APP_SRC_DIR}/currentcall.cpp) + ${APP_SRC_DIR}/currentcall.cpp + ${APP_SRC_DIR}/messageparser.cpp + ${APP_SRC_DIR}/previewengine.cpp) set(COMMON_HEADERS ${APP_SRC_DIR}/avatarimageprovider.h @@ -293,16 +299,9 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/callparticipantsmodel.h ${APP_SRC_DIR}/tipsmodel.h ${APP_SRC_DIR}/positioning.h - ${APP_SRC_DIR}/currentcall.h) - -if(WITH_WEBENGINE) - list(APPEND COMMON_SOURCES - ${APP_SRC_DIR}/previewengine.cpp) - add_definitions(-DWITH_WEBENGINE) -else() - list(APPEND COMMON_SOURCES - ${APP_SRC_DIR}/nowebengine/previewengine.cpp) -endif() + ${APP_SRC_DIR}/currentcall.h + ${APP_SRC_DIR}/messageparser.h + ${APP_SRC_DIR}/htmlparser.h) # For libavutil/avframe. set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib") @@ -494,10 +493,26 @@ if(ENABLE_LIBWRAP) ${LIBCLIENT_SRC_DIR}/qtwrapper/instancemanager_wrap.h) endif() +# SFPM set(BUILD_SFPM_PIC ON CACHE BOOL "enable -fPIC for SFPM" FORCE) add_subdirectory(3rdparty/SortFilterProxyModel) set(SFPM_OBJECTS $<TARGET_OBJECTS:SortFilterProxyModel>) +# md4c +set(BUILD_SHARED_LIBS OFF CACHE BOOL "Don't build shared md4c library" FORCE) +add_subdirectory(3rdparty/md4c EXCLUDE_FROM_ALL) +list(APPEND CLIENT_LINK_DIRS ${MD4C_BINARY_DIR}/src) +list(APPEND CLIENT_INCLUDE_DIRS ${MD4C_SOURCE_DIR}/src) +list(APPEND CLIENT_LIBS md4c-html) + +# tidy-html5 +set(BUILD_SHARED_LIB OFF CACHE BOOL "Don't build shared tidy library" FORCE) +set(SUPPORT_CONSOLE_APP OFF CACHE BOOL "Don't build tidy console app" FORCE) +add_subdirectory(3rdparty/tidy-html5 EXCLUDE_FROM_ALL) +list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release) +list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include) +list(APPEND CLIENT_LIBS tidy-static) + # common executable sources qt_add_executable( ${PROJECT_NAME} @@ -520,9 +535,7 @@ if(MSVC) PROPERTIES WIN32_EXECUTABLE TRUE) - target_link_libraries( - ${PROJECT_NAME} - PRIVATE + list(APPEND CLIENT_LIBS ${JAMID_LIB} ${GNUTLS_LIB} ${LIBCLIENT_NAME} @@ -559,9 +572,7 @@ if(MSVC) # executable name set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "Jami") elseif (NOT APPLE) - target_link_libraries( - ${PROJECT_NAME} - PRIVATE + list(APPEND CLIENT_LIBS ${QT_LIBS} ${LIBCLIENT_NAME} ${qrencode} @@ -693,13 +704,14 @@ else() set(libs ${libs} ${SPARKLE_FRAMEWORK}) endif(ENABLE_SPARKLE) target_sources(${PROJECT_NAME} PRIVATE ${resources}) - target_link_libraries(${PROJECT_NAME} PRIVATE ${libs}) - FILE(GLOB CONTRIB ${LIBJAMI_CONTRIB_DIR}/apple-darwin/lib/*.a) + list(APPEND CLIENT_LIBS ${libs}) + + file(GLOB CONTRIB ${LIBJAMI_CONTRIB_DIR}/apple-darwin/lib/*.a) + list(APPEND CLIENT_LIBS ${CONTRIB}) - target_link_libraries(${PROJECT_NAME} PRIVATE ${CONTRIB}) find_package(Iconv REQUIRED) - target_link_libraries(${PROJECT_NAME} PRIVATE Iconv::Iconv) - target_link_libraries(${PROJECT_NAME} PRIVATE + list(APPEND CLIENT_LIBS Iconv::Iconv) + list(APPEND CLIENT_LIBS "-framework AVFoundation" "-framework CoreAudio -framework CoreMedia -framework CoreVideo" "-framework VideoToolbox -framework AudioUnit" @@ -761,6 +773,10 @@ else() endif() endif() +target_include_directories(${PROJECT_NAME} PRIVATE ${CLIENT_INCLUDE_DIRS}) +target_link_directories(${PROJECT_NAME} PRIVATE ${CLIENT_LINK_DIRS}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${CLIENT_LIBS}) + qt_import_qml_plugins(${PROJECT_NAME}) qt_finalize_executable(${PROJECT_NAME}) diff --git a/extras/packaging/gnu-linux/Jenkinsfile b/extras/packaging/gnu-linux/Jenkinsfile index 87dff35bf4818e488d83e19a9b82c0b74255bf15..566a5bf49a5f5ee5077004fe42780ca62f9aab58 100644 --- a/extras/packaging/gnu-linux/Jenkinsfile +++ b/extras/packaging/gnu-linux/Jenkinsfile @@ -34,6 +34,8 @@ // Configuration globals. def SUBMODULES = ['daemon', '3rdparty/SortFilterProxyModel', + '3rdparty/md4c', + '3rdparty/tidy-html5', '3rdparty/qrencode-win32', 'extras/packaging/update/sparkle/Sparkle'] def TARGETS = [:] diff --git a/resources/webengine/linkify-string.js b/resources/webengine/linkify-string.js deleted file mode 100644 index b7fb03f8991333cda04175c308b5d94cbbb1e0e0..0000000000000000000000000000000000000000 --- a/resources/webengine/linkify-string.js +++ /dev/null @@ -1,100 +0,0 @@ -/* -* Copyright (c) 2021 SoapBox Innovations Inc. -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in -* all copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -* THE SOFTWARE. -*/ -var linkifyStr = (function (linkifyjs) { - 'use strict'; - - /** - Convert strings of text into linkable HTML text - */ - - function escapeText(text) { - return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); - } - - function escapeAttr(href) { - return href.replace(/"/g, '"'); - } - - function attributesToString(attributes) { - var result = []; - - for (var attr in attributes) { - var val = attributes[attr] + ''; - result.push(attr + "=\"" + escapeAttr(val) + "\""); - } - - return result.join(' '); - } - - function defaultRender(_ref) { - var tagName = _ref.tagName, - attributes = _ref.attributes, - content = _ref.content; - return "<" + tagName + " " + attributesToString(attributes) + ">" + escapeText(content) + "</" + tagName + ">"; - } - /** - * Convert a plan text string to an HTML string with links. Expects that the - * given strings does not contain any HTML entities. Use the linkify-html - * interface if you need to parse HTML entities. - * - * @param {string} str string to linkify - * @param {import('linkifyjs').Opts} [opts] overridable options - * @returns {string} - */ - - - function linkifyStr(str, opts) { - if (opts === void 0) { - opts = {}; - } - - opts = new linkifyjs.Options(opts, defaultRender); - var tokens = linkifyjs.tokenize(str); - var result = []; - - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - if (token.t === 'nl' && opts.get('nl2br')) { - result.push('<br>\n'); - } else if (!token.isLink || !opts.check(token)) { - result.push(escapeText(token.toString())); - } else { - result.push(opts.render(token)); - } - } - - return result.join(''); - } - - if (!String.prototype.linkify) { - Object.defineProperty(String.prototype, 'linkify', { - writable: false, - value: function linkify(options) { - return linkifyStr(this, options); - } - }); - } - - return linkifyStr; - -})(linkify); \ No newline at end of file diff --git a/resources/webengine/linkify.js b/resources/webengine/linkify.js deleted file mode 100644 index 7363cbdb42424d37879257ae390deeb917deeff7..0000000000000000000000000000000000000000 --- a/resources/webengine/linkify.js +++ /dev/null @@ -1,3501 +0,0 @@ -/* -* Copyright (c) 2021 SoapBox Innovations Inc. -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in -* all copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -* THE SOFTWARE. -*/ -var linkify = (function (exports) { - 'use strict'; - - // THIS FILE IS AUTOMATICALLY GENERATED DO NOT EDIT DIRECTLY - // https://data.iana.org/TLD/tlds-alpha-by-domain.txt - var tlds = 'aaa \ -aarp \ -abarth \ -abb \ -abbott \ -abbvie \ -abc \ -able \ -abogado \ -abudhabi \ -ac \ -academy \ -accenture \ -accountant \ -accountants \ -aco \ -actor \ -ad \ -adac \ -ads \ -adult \ -ae \ -aeg \ -aero \ -aetna \ -af \ -afl \ -africa \ -ag \ -agakhan \ -agency \ -ai \ -aig \ -airbus \ -airforce \ -airtel \ -akdn \ -al \ -alfaromeo \ -alibaba \ -alipay \ -allfinanz \ -allstate \ -ally \ -alsace \ -alstom \ -am \ -amazon \ -americanexpress \ -americanfamily \ -amex \ -amfam \ -amica \ -amsterdam \ -analytics \ -android \ -anquan \ -anz \ -ao \ -aol \ -apartments \ -app \ -apple \ -aq \ -aquarelle \ -ar \ -arab \ -aramco \ -archi \ -army \ -arpa \ -art \ -arte \ -as \ -asda \ -asia \ -associates \ -at \ -athleta \ -attorney \ -au \ -auction \ -audi \ -audible \ -audio \ -auspost \ -author \ -auto \ -autos \ -avianca \ -aw \ -aws \ -ax \ -axa \ -az \ -azure \ -ba \ -baby \ -baidu \ -banamex \ -bananarepublic \ -band \ -bank \ -bar \ -barcelona \ -barclaycard \ -barclays \ -barefoot \ -bargains \ -baseball \ -basketball \ -bauhaus \ -bayern \ -bb \ -bbc \ -bbt \ -bbva \ -bcg \ -bcn \ -bd \ -be \ -beats \ -beauty \ -beer \ -bentley \ -berlin \ -best \ -bestbuy \ -bet \ -bf \ -bg \ -bh \ -bharti \ -bi \ -bible \ -bid \ -bike \ -bing \ -bingo \ -bio \ -biz \ -bj \ -black \ -blackfriday \ -blockbuster \ -blog \ -bloomberg \ -blue \ -bm \ -bms \ -bmw \ -bn \ -bnpparibas \ -bo \ -boats \ -boehringer \ -bofa \ -bom \ -bond \ -boo \ -book \ -booking \ -bosch \ -bostik \ -boston \ -bot \ -boutique \ -box \ -br \ -bradesco \ -bridgestone \ -broadway \ -broker \ -brother \ -brussels \ -bs \ -bt \ -bugatti \ -build \ -builders \ -business \ -buy \ -buzz \ -bv \ -bw \ -by \ -bz \ -bzh \ -ca \ -cab \ -cafe \ -cal \ -call \ -calvinklein \ -cam \ -camera \ -camp \ -cancerresearch \ -canon \ -capetown \ -capital \ -capitalone \ -car \ -caravan \ -cards \ -care \ -career \ -careers \ -cars \ -casa \ -case \ -cash \ -casino \ -cat \ -catering \ -catholic \ -cba \ -cbn \ -cbre \ -cbs \ -cc \ -cd \ -center \ -ceo \ -cern \ -cf \ -cfa \ -cfd \ -cg \ -ch \ -chanel \ -channel \ -charity \ -chase \ -chat \ -cheap \ -chintai \ -christmas \ -chrome \ -church \ -ci \ -cipriani \ -circle \ -cisco \ -citadel \ -citi \ -citic \ -city \ -cityeats \ -ck \ -cl \ -claims \ -cleaning \ -click \ -clinic \ -clinique \ -clothing \ -cloud \ -club \ -clubmed \ -cm \ -cn \ -co \ -coach \ -codes \ -coffee \ -college \ -cologne \ -com \ -comcast \ -commbank \ -community \ -company \ -compare \ -computer \ -comsec \ -condos \ -construction \ -consulting \ -contact \ -contractors \ -cooking \ -cookingchannel \ -cool \ -coop \ -corsica \ -country \ -coupon \ -coupons \ -courses \ -cpa \ -cr \ -credit \ -creditcard \ -creditunion \ -cricket \ -crown \ -crs \ -cruise \ -cruises \ -cu \ -cuisinella \ -cv \ -cw \ -cx \ -cy \ -cymru \ -cyou \ -cz \ -dabur \ -dad \ -dance \ -data \ -date \ -dating \ -datsun \ -day \ -dclk \ -dds \ -de \ -deal \ -dealer \ -deals \ -degree \ -delivery \ -dell \ -deloitte \ -delta \ -democrat \ -dental \ -dentist \ -desi \ -design \ -dev \ -dhl \ -diamonds \ -diet \ -digital \ -direct \ -directory \ -discount \ -discover \ -dish \ -diy \ -dj \ -dk \ -dm \ -dnp \ -do \ -docs \ -doctor \ -dog \ -domains \ -dot \ -download \ -drive \ -dtv \ -dubai \ -dunlop \ -dupont \ -durban \ -dvag \ -dvr \ -dz \ -earth \ -eat \ -ec \ -eco \ -edeka \ -edu \ -education \ -ee \ -eg \ -email \ -emerck \ -energy \ -engineer \ -engineering \ -enterprises \ -epson \ -equipment \ -er \ -ericsson \ -erni \ -es \ -esq \ -estate \ -et \ -etisalat \ -eu \ -eurovision \ -eus \ -events \ -exchange \ -expert \ -exposed \ -express \ -extraspace \ -fage \ -fail \ -fairwinds \ -faith \ -family \ -fan \ -fans \ -farm \ -farmers \ -fashion \ -fast \ -fedex \ -feedback \ -ferrari \ -ferrero \ -fi \ -fiat \ -fidelity \ -fido \ -film \ -final \ -finance \ -financial \ -fire \ -firestone \ -firmdale \ -fish \ -fishing \ -fit \ -fitness \ -fj \ -fk \ -flickr \ -flights \ -flir \ -florist \ -flowers \ -fly \ -fm \ -fo \ -foo \ -food \ -foodnetwork \ -football \ -ford \ -forex \ -forsale \ -forum \ -foundation \ -fox \ -fr \ -free \ -fresenius \ -frl \ -frogans \ -frontdoor \ -frontier \ -ftr \ -fujitsu \ -fun \ -fund \ -furniture \ -futbol \ -fyi \ -ga \ -gal \ -gallery \ -gallo \ -gallup \ -game \ -games \ -gap \ -garden \ -gay \ -gb \ -gbiz \ -gd \ -gdn \ -ge \ -gea \ -gent \ -genting \ -george \ -gf \ -gg \ -ggee \ -gh \ -gi \ -gift \ -gifts \ -gives \ -giving \ -gl \ -glass \ -gle \ -global \ -globo \ -gm \ -gmail \ -gmbh \ -gmo \ -gmx \ -gn \ -godaddy \ -gold \ -goldpoint \ -golf \ -goo \ -goodyear \ -goog \ -google \ -gop \ -got \ -gov \ -gp \ -gq \ -gr \ -grainger \ -graphics \ -gratis \ -green \ -gripe \ -grocery \ -group \ -gs \ -gt \ -gu \ -guardian \ -gucci \ -guge \ -guide \ -guitars \ -guru \ -gw \ -gy \ -hair \ -hamburg \ -hangout \ -haus \ -hbo \ -hdfc \ -hdfcbank \ -health \ -healthcare \ -help \ -helsinki \ -here \ -hermes \ -hgtv \ -hiphop \ -hisamitsu \ -hitachi \ -hiv \ -hk \ -hkt \ -hm \ -hn \ -hockey \ -holdings \ -holiday \ -homedepot \ -homegoods \ -homes \ -homesense \ -honda \ -horse \ -hospital \ -host \ -hosting \ -hot \ -hoteles \ -hotels \ -hotmail \ -house \ -how \ -hr \ -hsbc \ -ht \ -hu \ -hughes \ -hyatt \ -hyundai \ -ibm \ -icbc \ -ice \ -icu \ -id \ -ie \ -ieee \ -ifm \ -ikano \ -il \ -im \ -imamat \ -imdb \ -immo \ -immobilien \ -in \ -inc \ -industries \ -infiniti \ -info \ -ing \ -ink \ -institute \ -insurance \ -insure \ -int \ -international \ -intuit \ -investments \ -io \ -ipiranga \ -iq \ -ir \ -irish \ -is \ -ismaili \ -ist \ -istanbul \ -it \ -itau \ -itv \ -jaguar \ -java \ -jcb \ -je \ -jeep \ -jetzt \ -jewelry \ -jio \ -jll \ -jm \ -jmp \ -jnj \ -jo \ -jobs \ -joburg \ -jot \ -joy \ -jp \ -jpmorgan \ -jprs \ -juegos \ -juniper \ -kaufen \ -kddi \ -ke \ -kerryhotels \ -kerrylogistics \ -kerryproperties \ -kfh \ -kg \ -kh \ -ki \ -kia \ -kids \ -kim \ -kinder \ -kindle \ -kitchen \ -kiwi \ -km \ -kn \ -koeln \ -komatsu \ -kosher \ -kp \ -kpmg \ -kpn \ -kr \ -krd \ -kred \ -kuokgroup \ -kw \ -ky \ -kyoto \ -kz \ -la \ -lacaixa \ -lamborghini \ -lamer \ -lancaster \ -lancia \ -land \ -landrover \ -lanxess \ -lasalle \ -lat \ -latino \ -latrobe \ -law \ -lawyer \ -lb \ -lc \ -lds \ -lease \ -leclerc \ -lefrak \ -legal \ -lego \ -lexus \ -lgbt \ -li \ -lidl \ -life \ -lifeinsurance \ -lifestyle \ -lighting \ -like \ -lilly \ -limited \ -limo \ -lincoln \ -linde \ -link \ -lipsy \ -live \ -living \ -lk \ -llc \ -llp \ -loan \ -loans \ -locker \ -locus \ -loft \ -lol \ -london \ -lotte \ -lotto \ -love \ -lpl \ -lplfinancial \ -lr \ -ls \ -lt \ -ltd \ -ltda \ -lu \ -lundbeck \ -luxe \ -luxury \ -lv \ -ly \ -ma \ -macys \ -madrid \ -maif \ -maison \ -makeup \ -man \ -management \ -mango \ -map \ -market \ -marketing \ -markets \ -marriott \ -marshalls \ -maserati \ -mattel \ -mba \ -mc \ -mckinsey \ -md \ -me \ -med \ -media \ -meet \ -melbourne \ -meme \ -memorial \ -men \ -menu \ -merckmsd \ -mg \ -mh \ -miami \ -microsoft \ -mil \ -mini \ -mint \ -mit \ -mitsubishi \ -mk \ -ml \ -mlb \ -mls \ -mm \ -mma \ -mn \ -mo \ -mobi \ -mobile \ -moda \ -moe \ -moi \ -mom \ -monash \ -money \ -monster \ -mormon \ -mortgage \ -moscow \ -moto \ -motorcycles \ -mov \ -movie \ -mp \ -mq \ -mr \ -ms \ -msd \ -mt \ -mtn \ -mtr \ -mu \ -museum \ -music \ -mutual \ -mv \ -mw \ -mx \ -my \ -mz \ -na \ -nab \ -nagoya \ -name \ -natura \ -navy \ -nba \ -nc \ -ne \ -nec \ -net \ -netbank \ -netflix \ -network \ -neustar \ -new \ -news \ -next \ -nextdirect \ -nexus \ -nf \ -nfl \ -ng \ -ngo \ -nhk \ -ni \ -nico \ -nike \ -nikon \ -ninja \ -nissan \ -nissay \ -nl \ -no \ -nokia \ -northwesternmutual \ -norton \ -now \ -nowruz \ -nowtv \ -np \ -nr \ -nra \ -nrw \ -ntt \ -nu \ -nyc \ -nz \ -obi \ -observer \ -office \ -okinawa \ -olayan \ -olayangroup \ -oldnavy \ -ollo \ -om \ -omega \ -one \ -ong \ -onl \ -online \ -ooo \ -open \ -oracle \ -orange \ -org \ -organic \ -origins \ -osaka \ -otsuka \ -ott \ -ovh \ -pa \ -page \ -panasonic \ -paris \ -pars \ -partners \ -parts \ -party \ -passagens \ -pay \ -pccw \ -pe \ -pet \ -pf \ -pfizer \ -pg \ -ph \ -pharmacy \ -phd \ -philips \ -phone \ -photo \ -photography \ -photos \ -physio \ -pics \ -pictet \ -pictures \ -pid \ -pin \ -ping \ -pink \ -pioneer \ -pizza \ -pk \ -pl \ -place \ -play \ -playstation \ -plumbing \ -plus \ -pm \ -pn \ -pnc \ -pohl \ -poker \ -politie \ -porn \ -post \ -pr \ -pramerica \ -praxi \ -press \ -prime \ -pro \ -prod \ -productions \ -prof \ -progressive \ -promo \ -properties \ -property \ -protection \ -pru \ -prudential \ -ps \ -pt \ -pub \ -pw \ -pwc \ -py \ -qa \ -qpon \ -quebec \ -quest \ -racing \ -radio \ -re \ -read \ -realestate \ -realtor \ -realty \ -recipes \ -red \ -redstone \ -redumbrella \ -rehab \ -reise \ -reisen \ -reit \ -reliance \ -ren \ -rent \ -rentals \ -repair \ -report \ -republican \ -rest \ -restaurant \ -review \ -reviews \ -rexroth \ -rich \ -richardli \ -ricoh \ -ril \ -rio \ -rip \ -ro \ -rocher \ -rocks \ -rodeo \ -rogers \ -room \ -rs \ -rsvp \ -ru \ -rugby \ -ruhr \ -run \ -rw \ -rwe \ -ryukyu \ -sa \ -saarland \ -safe \ -safety \ -sakura \ -sale \ -salon \ -samsclub \ -samsung \ -sandvik \ -sandvikcoromant \ -sanofi \ -sap \ -sarl \ -sas \ -save \ -saxo \ -sb \ -sbi \ -sbs \ -sc \ -sca \ -scb \ -schaeffler \ -schmidt \ -scholarships \ -school \ -schule \ -schwarz \ -science \ -scot \ -sd \ -se \ -search \ -seat \ -secure \ -security \ -seek \ -select \ -sener \ -services \ -ses \ -seven \ -sew \ -sex \ -sexy \ -sfr \ -sg \ -sh \ -shangrila \ -sharp \ -shaw \ -shell \ -shia \ -shiksha \ -shoes \ -shop \ -shopping \ -shouji \ -show \ -showtime \ -si \ -silk \ -sina \ -singles \ -site \ -sj \ -sk \ -ski \ -skin \ -sky \ -skype \ -sl \ -sling \ -sm \ -smart \ -smile \ -sn \ -sncf \ -so \ -soccer \ -social \ -softbank \ -software \ -sohu \ -solar \ -solutions \ -song \ -sony \ -soy \ -spa \ -space \ -sport \ -spot \ -sr \ -srl \ -ss \ -st \ -stada \ -staples \ -star \ -statebank \ -statefarm \ -stc \ -stcgroup \ -stockholm \ -storage \ -store \ -stream \ -studio \ -study \ -style \ -su \ -sucks \ -supplies \ -supply \ -support \ -surf \ -surgery \ -suzuki \ -sv \ -swatch \ -swiss \ -sx \ -sy \ -sydney \ -systems \ -sz \ -tab \ -taipei \ -talk \ -taobao \ -target \ -tatamotors \ -tatar \ -tattoo \ -tax \ -taxi \ -tc \ -tci \ -td \ -tdk \ -team \ -tech \ -technology \ -tel \ -temasek \ -tennis \ -teva \ -tf \ -tg \ -th \ -thd \ -theater \ -theatre \ -tiaa \ -tickets \ -tienda \ -tiffany \ -tips \ -tires \ -tirol \ -tj \ -tjmaxx \ -tjx \ -tk \ -tkmaxx \ -tl \ -tm \ -tmall \ -tn \ -to \ -today \ -tokyo \ -tools \ -top \ -toray \ -toshiba \ -total \ -tours \ -town \ -toyota \ -toys \ -tr \ -trade \ -trading \ -training \ -travel \ -travelchannel \ -travelers \ -travelersinsurance \ -trust \ -trv \ -tt \ -tube \ -tui \ -tunes \ -tushu \ -tv \ -tvs \ -tw \ -tz \ -ua \ -ubank \ -ubs \ -ug \ -uk \ -unicom \ -university \ -uno \ -uol \ -ups \ -us \ -uy \ -uz \ -va \ -vacations \ -vana \ -vanguard \ -vc \ -ve \ -vegas \ -ventures \ -verisign \ -vermögensberater \ -vermögensberatung \ -versicherung \ -vet \ -vg \ -vi \ -viajes \ -video \ -vig \ -viking \ -villas \ -vin \ -vip \ -virgin \ -visa \ -vision \ -viva \ -vivo \ -vlaanderen \ -vn \ -vodka \ -volkswagen \ -volvo \ -vote \ -voting \ -voto \ -voyage \ -vu \ -vuelos \ -wales \ -walmart \ -walter \ -wang \ -wanggou \ -watch \ -watches \ -weather \ -weatherchannel \ -webcam \ -weber \ -website \ -wed \ -wedding \ -weibo \ -weir \ -wf \ -whoswho \ -wien \ -wiki \ -williamhill \ -win \ -windows \ -wine \ -winners \ -wme \ -wolterskluwer \ -woodside \ -work \ -works \ -world \ -wow \ -ws \ -wtc \ -wtf \ -xbox \ -xerox \ -xfinity \ -xihuan \ -xin \ -xxx \ -xyz \ -yachts \ -yahoo \ -yamaxun \ -yandex \ -ye \ -yodobashi \ -yoga \ -yokohama \ -you \ -youtube \ -yt \ -yun \ -za \ -zappos \ -zara \ -zero \ -zip \ -zm \ -zone \ -zuerich \ -zw'.split(' '); // Internationalized domain names containing non-ASCII - - var utlds = 'ελ \ -ευ \ -бг \ -бел \ -дети \ -ею \ -католик \ -ком \ -мкд \ -мон \ -моÑква \ -онлайн \ -орг \ -Ñ€ÑƒÑ \ -рф \ -Ñайт \ -Ñрб \ -укр \ -қаз \ -Õ°Õ¡Õµ \ -ישר×ל \ -×§×•× \ -ابوظبي \ -اتصالات \ -ارامكو \ -الاردن \ -Ø§Ù„Ø¨ØØ±ÙŠÙ† \ -الجزائر \ -السعودية \ -العليان \ -المغرب \ -امارات \ -ایران \ -بارت \ -بازار \ -بيتك \ -بھارت \ -تونس \ -سودان \ -سورية \ -شبكة \ -عراق \ -عرب \ -عمان \ -Ùلسطين \ -قطر \ -كاثوليك \ -كوم \ -مصر \ -مليسيا \ -موريتانيا \ -موقع \ -همراه \ -پاکستان \ -ڀارت \ -कॉम \ -नेट \ -à¤à¤¾à¤°à¤¤ \ -à¤à¤¾à¤°à¤¤à¤®à¥ \ -à¤à¤¾à¤°à¥‹à¤¤ \ -संगठन \ -বাংলা \ -à¦à¦¾à¦°à¦¤ \ -à¦à¦¾à§°à¦¤ \ -à¨à¨¾à¨°à¨¤ \ -àªàª¾àª°àª¤ \ -à¬à¬¾à¬°à¬¤ \ -இநà¯à®¤à®¿à®¯à®¾ \ -இலஙà¯à®•ை \ -சிஙà¯à®•பà¯à®ªà¯‚ர௠\ -à°à°¾à°°à°¤à± \ -à²à²¾à²°à²¤ \ -à´à´¾à´°à´¤à´‚ \ -ලංක෠\ -คà¸à¸¡ \ -ไทย \ -ລາວ \ -გე \ -ã¿ã‚“㪠\ -アマゾン \ -クラウド \ -グーグル \ -コム\ -ストア \ -セール \ -ファッション \ -ãƒã‚¤ãƒ³ãƒˆ \ -世界 \ -ä¸ä¿¡ \ -ä¸å›½ \ -ä¸åœ‹ \ -䏿–‡ç½‘ \ -亚马逊 \ -ä¼ä¸š \ -佛山 \ -ä¿¡æ¯ \ -å¥åº· \ -å…«å¦ \ -å…¬å¸ \ -公益 \ -å°æ¹¾ \ -å°ç£ \ -商城 \ -商店 \ -å•†æ ‡ \ -嘉里 \ -嘉里大酒店 \ -在线 \ -大拿 \ -天主教 \ -å¨±ä¹ \ -å®¶é›» \ -广东 \ -å¾®åš \ -慈善 \ -æˆ‘çˆ±ä½ \ -手机 \ -æ‹›è˜ \ -政务 \ -政府 \ -æ–°åŠ å¡ \ -æ–°é—» \ -æ—¶å°š \ -æ›¸ç± \ -机构 \ -淡马锡 \ -æ¸¸æˆ \ -澳門 \ -点看 \ -移动 \ -组织机构 \ -ç½‘å€ \ -网店 \ -网站 \ -网络 \ -è”通 \ -诺基亚 \ -è°·æŒ \ -è´ç‰© \ -通販 \ -集团 \ -電訊盈科 \ -飞利浦 \ -é£Ÿå“ \ -é¤åŽ… \ -é¦™æ ¼é‡Œæ‹‰ \ -香港 \ -ë‹·ë„· \ -ë‹·ì»´ \ -삼성 \ -한êµ'.split(' '); - - /** - * @template A - * @template B - * @param {A} target - * @param {B} properties - * @return {A & B} - */ - var assign = function assign(target, properties) { - for (var key in properties) { - target[key] = properties[key]; - } - - return target; - }; - - /** - * Finite State Machine generation utilities - */ - /** - * @template T - * @typedef {{ [group: string]: T[] }} Collections - */ - - /** - * @typedef {{ [group: string]: true }} Flags - */ - // Keys in scanner Collections instances - - var numeric = 'numeric'; - var ascii = 'ascii'; - var alpha = 'alpha'; - var asciinumeric = 'asciinumeric'; - var alphanumeric = 'alphanumeric'; - var domain = 'domain'; - var emoji = 'emoji'; - var scheme = 'scheme'; - var slashscheme = 'slashscheme'; - var whitespace = 'whitespace'; - /** - * @template T - * @param {string} name - * @param {Collections<T>} groups to register in - * @returns {T[]} Current list of tokens in the given collection - */ - - function registerGroup(name, groups) { - if (!(name in groups)) { - groups[name] = []; - } - - return groups[name]; - } - /** - * @template T - * @param {T} t token to add - * @param {Collections<T>} groups - * @param {Flags} flags - */ - - - function addToGroups(t, flags, groups) { - if (flags[numeric]) { - flags[asciinumeric] = true; - flags[alphanumeric] = true; - } - - if (flags[ascii]) { - flags[asciinumeric] = true; - flags[alpha] = true; - } - - if (flags[asciinumeric]) { - flags[alphanumeric] = true; - } - - if (flags[alpha]) { - flags[alphanumeric] = true; - } - - if (flags[alphanumeric]) { - flags[domain] = true; - } - - if (flags[emoji]) { - flags[domain] = true; - } - - for (var k in flags) { - var group = registerGroup(k, groups); - - if (group.indexOf(t) < 0) { - group.push(t); - } - } - } - /** - * @template T - * @param {T} t token to check - * @param {Collections<T>} groups - * @returns {Flags} group flags that contain this token - */ - - function flagsForToken(t, groups) { - var result = {}; - - for (var c in groups) { - if (groups[c].indexOf(t) >= 0) { - result[c] = true; - } - } - - return result; - } - /** - * @template T - * @typedef {null | T } Transition - */ - - /** - * Define a basic state machine state. j is the list of character transitions, - * jr is the list of regex-match transitions, jd is the default state to - * transition to t is the accepting token type, if any. If this is the terminal - * state, then it does not emit a token. - * - * The template type T represents the type of the token this state accepts. This - * should be a string (such as of the token exports in `text.js`) or a - * MultiToken subclass (from `multi.js`) - * - * @template T - * @param {T} [token] Token that this state emits - */ - - - function State(token) { - if (token === void 0) { - token = null; - } - - // this.n = null; // DEBUG: State name - - /** @type {{ [input: string]: State<T> }} j */ - this.j = {}; // IMPLEMENTATION 1 - // this.j = []; // IMPLEMENTATION 2 - - /** @type {[RegExp, State<T>][]} jr */ - - this.jr = []; - /** @type {?State<T>} jd */ - - this.jd = null; - /** @type {?T} t */ - - this.t = token; - } - /** - * Scanner token groups - * @type Collections<string> - */ - - State.groups = {}; - State.prototype = { - accepts: function accepts() { - return !!this.t; - }, - - /** - * Follow an existing transition from the given input to the next state. - * Does not mutate. - * @param {string} input character or token type to transition on - * @returns {?State<T>} the next state, if any - */ - go: function go(input) { - var state = this; - var nextState = state.j[input]; - - if (nextState) { - return nextState; - } - - for (var i = 0; i < state.jr.length; i++) { - var regex = state.jr[i][0]; - var _nextState = state.jr[i][1]; // note: might be empty to prevent default jump - - if (_nextState && regex.test(input)) { - return _nextState; - } - } // Nowhere left to jump! Return default, if any - - - return state.jd; - }, - - /** - * Whether the state has a transition for the given input. Set the second - * argument to true to only look for an exact match (and not a default or - * regular-expression-based transition) - * @param {string} input - * @param {boolean} exactOnly - */ - has: function has(input, exactOnly) { - if (exactOnly === void 0) { - exactOnly = false; - } - - return exactOnly ? input in this.j : !!this.go(input); - }, - - /** - * Short for "transition all"; create a transition from the array of items - * in the given list to the same final resulting state. - * @param {string | string[]} inputs Group of inputs to transition on - * @param {Transition<T> | State<T>} [next] Transition options - * @param {Flags} [flags] Collections flags to add token to - * @param {Collections<T>} [groups] Master list of token groups - */ - ta: function ta(inputs, next, flags, groups) { - for (var i = 0; i < inputs.length; i++) { - this.tt(inputs[i], next, flags, groups); - } - }, - - /** - * Short for "take regexp transition"; defines a transition for this state - * when it encounters a token which matches the given regular expression - * @param {RegExp} regexp Regular expression transition (populate first) - * @param {T | State<T>} [next] Transition options - * @param {Flags} [flags] Collections flags to add token to - * @param {Collections<T>} [groups] Master list of token groups - * @returns {State<T>} taken after the given input - */ - tr: function tr(regexp, next, flags, groups) { - groups = groups || State.groups; - var nextState; - - if (next && next.j) { - nextState = next; - } else { - // Token with maybe token groups - nextState = new State(next); - - if (flags && groups) { - addToGroups(next, flags, groups); - } - } - - this.jr.push([regexp, nextState]); - return nextState; - }, - - /** - * Short for "take transitions", will take as many sequential transitions as - * the length of the given input and returns the - * resulting final state. - * @param {string | string[]} input - * @param {T | State<T>} [next] Transition options - * @param {Flags} [flags] Collections flags to add token to - * @param {Collections<T>} [groups] Master list of token groups - * @returns {State<T>} taken after the given input - */ - ts: function ts(input, next, flags, groups) { - var state = this; - var len = input.length; - - if (!len) { - return state; - } - - for (var i = 0; i < len - 1; i++) { - state = state.tt(input[i]); - } - - return state.tt(input[len - 1], next, flags, groups); - }, - - /** - * Short for "take transition", this is a method for building/working with - * state machines. - * - * If a state already exists for the given input, returns it. - * - * If a token is specified, that state will emit that token when reached by - * the linkify engine. - * - * If no state exists, it will be initialized with some default transitions - * that resemble existing default transitions. - * - * If a state is given for the second argument, that state will be - * transitioned to on the given input regardless of what that input - * previously did. - * - * Specify a token group flags to define groups that this token belongs to. - * The token will be added to corresponding entires in the given groups - * object. - * - * @param {string} input character, token type to transition on - * @param {T | State<T>} [next] Transition options - * @param {Flags} [flags] Collections flags to add token to - * @param {Collections<T>} [groups] Master list of groups - * @returns {State<T>} taken after the given input - */ - tt: function tt(input, next, flags, groups) { - groups = groups || State.groups; - var state = this; // Check if existing state given, just a basic transition - - if (next && next.j) { - state.j[input] = next; - return next; - } - - var t = next; // Take the transition with the usual default mechanisms and use that as - // a template for creating the next state - - var nextState, - templateState = state.go(input); - - if (templateState) { - nextState = new State(); - assign(nextState.j, templateState.j); - nextState.jr.push.apply(nextState.jr, templateState.jr); - nextState.jd = templateState.jd; - nextState.t = templateState.t; - } else { - nextState = new State(); - } - - if (t) { - // Ensure newly token is in the same groups as the old token - if (groups) { - if (nextState.t && typeof nextState.t === 'string') { - var allFlags = assign(flagsForToken(nextState.t, groups), flags); - addToGroups(t, allFlags, groups); - } else if (flags) { - addToGroups(t, flags, groups); - } - } - - nextState.t = t; // overwrite anything that was previously there - } - - state.j[input] = nextState; - return nextState; - } - }; // Helper functions to improve minification (not exported outside linkifyjs module) - - /** - * @template T - * @param {State<T>} state - * @param {string | string[]} input - * @param {Flags} [flags] - * @param {Collections<T>} [groups] - */ - - var ta = function ta(state, input, next, flags, groups) { - return state.ta(input, next, flags, groups); - }; - /** - * @template T - * @param {State<T>} state - * @param {RegExp} regexp - * @param {T | State<T>} [next] - * @param {Flags} [flags] - * @param {Collections<T>} [groups] - */ - - var tr = function tr(state, regexp, next, flags, groups) { - return state.tr(regexp, next, flags, groups); - }; - /** - * @template T - * @param {State<T>} state - * @param {string | string[]} input - * @param {T | State<T>} [next] - * @param {Flags} [flags] - * @param {Collections<T>} [groups] - */ - - var ts = function ts(state, input, next, flags, groups) { - return state.ts(input, next, flags, groups); - }; - /** - * @template T - * @param {State<T>} state - * @param {string} input - * @param {T | State<T>} [next] - * @param {Collections<T>} [groups] - * @param {Flags} [flags] - */ - - var tt = function tt(state, input, next, flags, groups) { - return state.tt(input, next, flags, groups); - }; - - /****************************************************************************** - Text Tokens - Identifiers for token outputs from the regexp scanner - ******************************************************************************/ - // A valid web domain token - var WORD = 'WORD'; // only contains a-z - - var UWORD = 'UWORD'; // contains letters other than a-z, used for IDN - // Special case of word - - var LOCALHOST = 'LOCALHOST'; // Valid top-level domain, special case of WORD (see tlds.js) - - var TLD = 'TLD'; // Valid IDN TLD, special case of UWORD (see tlds.js) - - var UTLD = 'UTLD'; // The scheme portion of a web URI protocol. Supported types include: `mailto`, - // `file`, and user-defined custom protocols. Limited to schemes that contain - // only letters - - var SCHEME = 'SCHEME'; // Similar to SCHEME, except makes distinction for schemes that must always be - // followed by `://`, not just `:`. Supported types include `http`, `https`, - // `ftp`, `ftps` - - var SLASH_SCHEME = 'SLASH_SCHEME'; // Any sequence of digits 0-9 - - var NUM = 'NUM'; // Any number of consecutive whitespace characters that are not newline - - var WS = 'WS'; // New line (unix style) - - var NL$1 = 'NL'; // \n - // Opening/closing bracket classes - - var OPENBRACE = 'OPENBRACE'; // { - - var OPENBRACKET = 'OPENBRACKET'; // [ - - var OPENANGLEBRACKET = 'OPENANGLEBRACKET'; // < - - var OPENPAREN = 'OPENPAREN'; // ( - - var CLOSEBRACE = 'CLOSEBRACE'; // } - - var CLOSEBRACKET = 'CLOSEBRACKET'; // ] - - var CLOSEANGLEBRACKET = 'CLOSEANGLEBRACKET'; // > - - var CLOSEPAREN = 'CLOSEPAREN'; // ) - // Various symbols - - var AMPERSAND = 'AMPERSAND'; // & - - var APOSTROPHE = 'APOSTROPHE'; // ' - - var ASTERISK = 'ASTERISK'; // * - - var AT = 'AT'; // @ - - var BACKSLASH = 'BACKSLASH'; // \ - - var BACKTICK = 'BACKTICK'; // ` - - var CARET = 'CARET'; // ^ - - var COLON = 'COLON'; // : - - var COMMA = 'COMMA'; // , - - var DOLLAR = 'DOLLAR'; // $ - - var DOT = 'DOT'; // . - - var EQUALS = 'EQUALS'; // = - - var EXCLAMATION = 'EXCLAMATION'; // ! - - var HYPHEN = 'HYPHEN'; // - - - var PERCENT = 'PERCENT'; // % - - var PIPE = 'PIPE'; // | - - var PLUS = 'PLUS'; // + - - var POUND = 'POUND'; // # - - var QUERY = 'QUERY'; // ? - - var QUOTE = 'QUOTE'; // " - - var SEMI = 'SEMI'; // ; - - var SLASH = 'SLASH'; // / - - var TILDE = 'TILDE'; // ~ - - var UNDERSCORE = 'UNDERSCORE'; // _ - // Emoji symbol - - var EMOJI$1 = 'EMOJI'; // Default token - anything that is not one of the above - - var SYM = 'SYM'; - - var tk = /*#__PURE__*/Object.freeze({ - __proto__: null, - WORD: WORD, - UWORD: UWORD, - LOCALHOST: LOCALHOST, - TLD: TLD, - UTLD: UTLD, - SCHEME: SCHEME, - SLASH_SCHEME: SLASH_SCHEME, - NUM: NUM, - WS: WS, - NL: NL$1, - OPENBRACE: OPENBRACE, - OPENBRACKET: OPENBRACKET, - OPENANGLEBRACKET: OPENANGLEBRACKET, - OPENPAREN: OPENPAREN, - CLOSEBRACE: CLOSEBRACE, - CLOSEBRACKET: CLOSEBRACKET, - CLOSEANGLEBRACKET: CLOSEANGLEBRACKET, - CLOSEPAREN: CLOSEPAREN, - AMPERSAND: AMPERSAND, - APOSTROPHE: APOSTROPHE, - ASTERISK: ASTERISK, - AT: AT, - BACKSLASH: BACKSLASH, - BACKTICK: BACKTICK, - CARET: CARET, - COLON: COLON, - COMMA: COMMA, - DOLLAR: DOLLAR, - DOT: DOT, - EQUALS: EQUALS, - EXCLAMATION: EXCLAMATION, - HYPHEN: HYPHEN, - PERCENT: PERCENT, - PIPE: PIPE, - PLUS: PLUS, - POUND: POUND, - QUERY: QUERY, - QUOTE: QUOTE, - SEMI: SEMI, - SLASH: SLASH, - TILDE: TILDE, - UNDERSCORE: UNDERSCORE, - EMOJI: EMOJI$1, - SYM: SYM - }); - - // Note that these two Unicode ones expand into a really big one with Babel - var ASCII_LETTER = /[a-z]/; - var LETTER = /(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/; // Any Unicode character with letter data type - - var EMOJI = /(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEDD-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDDFF\uDE70-\uDE74\uDE78-\uDE7C\uDE80-\uDE86\uDE90-\uDEAC\uDEB0-\uDEBA\uDEC0-\uDEC5\uDED0-\uDED9\uDEE0-\uDEE7\uDEF0-\uDEF6])/; // Any Unicode emoji character - - var EMOJI_VARIATION$1 = /\ufe0f/; - var DIGIT = /\d/; - var SPACE = /\s/; - - var regexp = /*#__PURE__*/Object.freeze({ - __proto__: null, - ASCII_LETTER: ASCII_LETTER, - LETTER: LETTER, - EMOJI: EMOJI, - EMOJI_VARIATION: EMOJI_VARIATION$1, - DIGIT: DIGIT, - SPACE: SPACE - }); - - /** - The scanner provides an interface that takes a string of text as input, and - outputs an array of tokens instances that can be used for easy URL parsing. - */ - var NL = '\n'; // New line character - - var EMOJI_VARIATION = "\uFE0F"; // Variation selector, follows heart and others - - var EMOJI_JOINER = "\u200D"; // zero-width joiner - - /** - * Scanner output token: - * - `t` is the token name (e.g., 'NUM', 'EMOJI', 'TLD') - * - `v` is the value of the token (e.g., '123', 'â¤ï¸', 'com') - * - `s` is the start index of the token in the original string - * - `e` is the end index of the token in the original string - * @typedef {{t: string, v: string, s: number, e: number}} Token - */ - - /** - * @template T - * @typedef {{ [collection: string]: T[] }} Collections - */ - - /** - * Initialize the scanner character-based state machine for the given start - * state - * @param {[string, boolean][]} customSchemes List of custom schemes, where each - * item is a length-2 tuple with the first element set to the string scheme, and - * the second element set to `true` if the `://` after the scheme is optional - */ - - function init$2(customSchemes) { - var _tr, _tr2, _tr3, _tr4, _tt, _tr5; - - if (customSchemes === void 0) { - customSchemes = []; - } - - // Frequently used states (name argument removed during minification) - - /** @type Collections<string> */ - var groups = {}; // of tokens - - State.groups = groups; - /** @type State<string> */ - - var Start = new State(); // States for special URL symbols that accept immediately after start - - tt(Start, "'", APOSTROPHE); - tt(Start, '{', OPENBRACE); - tt(Start, '[', OPENBRACKET); - tt(Start, '<', OPENANGLEBRACKET); - tt(Start, '(', OPENPAREN); - tt(Start, '}', CLOSEBRACE); - tt(Start, ']', CLOSEBRACKET); - tt(Start, '>', CLOSEANGLEBRACKET); - tt(Start, ')', CLOSEPAREN); - tt(Start, '&', AMPERSAND); - tt(Start, '*', ASTERISK); - tt(Start, '@', AT); - tt(Start, '`', BACKTICK); - tt(Start, '^', CARET); - tt(Start, ':', COLON); - tt(Start, ',', COMMA); - tt(Start, '$', DOLLAR); - tt(Start, '.', DOT); - tt(Start, '=', EQUALS); - tt(Start, '!', EXCLAMATION); - tt(Start, '-', HYPHEN); - tt(Start, '%', PERCENT); - tt(Start, '|', PIPE); - tt(Start, '+', PLUS); - tt(Start, '#', POUND); - tt(Start, '?', QUERY); - tt(Start, '"', QUOTE); - tt(Start, '/', SLASH); - tt(Start, ';', SEMI); - tt(Start, '~', TILDE); - tt(Start, '_', UNDERSCORE); - tt(Start, '\\', BACKSLASH); - var Num = tr(Start, DIGIT, NUM, (_tr = {}, _tr[numeric] = true, _tr)); - tr(Num, DIGIT, Num); // State which emits a word token - - var Word = tr(Start, ASCII_LETTER, WORD, (_tr2 = {}, _tr2[ascii] = true, _tr2)); - tr(Word, ASCII_LETTER, Word); // Same as previous, but specific to non-fsm.ascii alphabet words - - var UWord = tr(Start, LETTER, UWORD, (_tr3 = {}, _tr3[alpha] = true, _tr3)); - tr(UWord, ASCII_LETTER); // Non-accepting - - tr(UWord, LETTER, UWord); // Whitespace jumps - // Tokens of only non-newline whitespace are arbitrarily long - // If any whitespace except newline, more whitespace! - - var Ws = tr(Start, SPACE, WS, (_tr4 = {}, _tr4[whitespace] = true, _tr4)); - tt(Start, NL, NL$1, (_tt = {}, _tt[whitespace] = true, _tt)); - tt(Ws, NL); // non-accepting state to avoid mixing whitespaces - - tr(Ws, SPACE, Ws); // Emoji tokens. They are not grouped by the scanner except in cases where a - // zero-width joiner is present - - var Emoji = tr(Start, EMOJI, EMOJI$1, (_tr5 = {}, _tr5[emoji] = true, _tr5)); - tr(Emoji, EMOJI, Emoji); - tt(Emoji, EMOJI_VARIATION, Emoji); // tt(Start, EMOJI_VARIATION, Emoji); // This one is sketchy - - var EmojiJoiner = tt(Emoji, EMOJI_JOINER); - tr(EmojiJoiner, EMOJI, Emoji); // tt(EmojiJoiner, EMOJI_VARIATION, Emoji); // also sketchy - // Generates states for top-level domains - // Note that this is most accurate when tlds are in alphabetical order - - var wordjr = [[ASCII_LETTER, Word]]; - var uwordjr = [[ASCII_LETTER, null], [LETTER, UWord]]; - - for (var i = 0; i < tlds.length; i++) { - fastts(Start, tlds[i], TLD, WORD, wordjr); - } - - for (var _i = 0; _i < utlds.length; _i++) { - fastts(Start, utlds[_i], UTLD, UWORD, uwordjr); - } - - addToGroups(TLD, { - tld: true, - ascii: true - }, groups); - addToGroups(UTLD, { - utld: true, - alpha: true - }, groups); // Collect the states generated by different protocols. NOTE: If any new TLDs - // get added that are also protocols, set the token to be the same as the - // protocol to ensure parsing works as expected. - - fastts(Start, 'file', SCHEME, WORD, wordjr); - fastts(Start, 'mailto', SCHEME, WORD, wordjr); - fastts(Start, 'http', SLASH_SCHEME, WORD, wordjr); - fastts(Start, 'https', SLASH_SCHEME, WORD, wordjr); - fastts(Start, 'ftp', SLASH_SCHEME, WORD, wordjr); - fastts(Start, 'ftps', SLASH_SCHEME, WORD, wordjr); - addToGroups(SCHEME, { - scheme: true, - ascii: true - }, groups); - addToGroups(SLASH_SCHEME, { - slashscheme: true, - ascii: true - }, groups); // Register custom schemes. Assumes each scheme is asciinumeric with hyphens - - customSchemes = customSchemes.sort(function (a, b) { - return a[0] > b[0] ? 1 : -1; - }); - - for (var _i2 = 0; _i2 < customSchemes.length; _i2++) { - var _ref, _ref2; - - var sch = customSchemes[_i2][0]; - var optionalSlashSlash = customSchemes[_i2][1]; - var flags = optionalSlashSlash ? (_ref = {}, _ref[scheme] = true, _ref) : (_ref2 = {}, _ref2[slashscheme] = true, _ref2); - - if (sch.indexOf('-') >= 0) { - flags[domain] = true; - } else if (!ASCII_LETTER.test(sch)) { - flags[numeric] = true; // numbers only - } else if (DIGIT.test(sch)) { - flags[asciinumeric] = true; - } else { - flags[ascii] = true; - } - - ts(Start, sch, sch, flags); - } // Localhost token - - - ts(Start, 'localhost', LOCALHOST, { - ascii: true - }); // Set default transition for start state (some symbol) - - Start.jd = new State(SYM); - return { - start: Start, - tokens: assign({ - groups: groups - }, tk) - }; - } - /** - Given a string, returns an array of TOKEN instances representing the - composition of that string. - - @method run - @param {State<string>} start scanner starting state - @param {string} str input string to scan - @return {Token[]} list of tokens, each with a type and value - */ - - function run$1(start, str) { - // State machine is not case sensitive, so input is tokenized in lowercased - // form (still returns regular case). Uses selective `toLowerCase` because - // lowercasing the entire string causes the length and character position to - // vary in some non-English strings with V8-based runtimes. - var iterable = stringToArray(str.replace(/[A-Z]/g, function (c) { - return c.toLowerCase(); - })); - var charCount = iterable.length; // <= len if there are emojis, etc - - var tokens = []; // return value - // cursor through the string itself, accounting for characters that have - // width with length 2 such as emojis - - var cursor = 0; // Cursor through the array-representation of the string - - var charCursor = 0; // Tokenize the string - - while (charCursor < charCount) { - var state = start; - var nextState = null; - var tokenLength = 0; - var latestAccepting = null; - var sinceAccepts = -1; - var charsSinceAccepts = -1; - - while (charCursor < charCount && (nextState = state.go(iterable[charCursor]))) { - state = nextState; // Keep track of the latest accepting state - - if (state.accepts()) { - sinceAccepts = 0; - charsSinceAccepts = 0; - latestAccepting = state; - } else if (sinceAccepts >= 0) { - sinceAccepts += iterable[charCursor].length; - charsSinceAccepts++; - } - - tokenLength += iterable[charCursor].length; - cursor += iterable[charCursor].length; - charCursor++; - } // Roll back to the latest accepting state - - - cursor -= sinceAccepts; - charCursor -= charsSinceAccepts; - tokenLength -= sinceAccepts; // No more jumps, just make a new token from the last accepting one - - tokens.push({ - t: latestAccepting.t, - // token type/name - v: str.slice(cursor - tokenLength, cursor), - // string value - s: cursor - tokenLength, - // start index - e: cursor // end index (excluding) - - }); - } - - return tokens; - } - /** - * Convert a String to an Array of characters, taking into account that some - * characters like emojis take up two string indexes. - * - * Adapted from core-js (MIT license) - * https://github.com/zloirock/core-js/blob/2d69cf5f99ab3ea3463c395df81e5a15b68f49d9/packages/core-js/internals/string-multibyte.js - * - * @function stringToArray - * @param {string} str - * @returns {string[]} - */ - - function stringToArray(str) { - var result = []; - var len = str.length; - var index = 0; - - while (index < len) { - var first = str.charCodeAt(index); - var second = void 0; - var char = first < 0xd800 || first > 0xdbff || index + 1 === len || (second = str.charCodeAt(index + 1)) < 0xdc00 || second > 0xdfff ? str[index] // single character - : str.slice(index, index + 2); // two-index characters - - result.push(char); - index += char.length; - } - - return result; - } - /** - * Fast version of ts function for when transition defaults are well known - * @param {State<string>} state - * @param {string} input - * @param {string} t - * @param {string} defaultt - * @param {[RegExp, State<string>][]} jr - * @returns {State<string>} - */ - - function fastts(state, input, t, defaultt, jr) { - var next; - var len = input.length; - - for (var i = 0; i < len - 1; i++) { - var char = input[i]; - - if (state.j[char]) { - next = state.j[char]; - } else { - next = new State(defaultt); - next.jr = jr.slice(); - state.j[char] = next; - } - - state = next; - } - - next = new State(t); - next.jr = jr.slice(); - state.j[input[len - 1]] = next; - return next; - } - - function _inheritsLoose(subClass, superClass) { - subClass.prototype = Object.create(superClass.prototype); - subClass.prototype.constructor = subClass; - - _setPrototypeOf(subClass, superClass); - } - - function _setPrototypeOf(o, p) { - _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; - - return _setPrototypeOf(o, p); - } - - /** - * An object where each key is a valid DOM Event Name such as `click` or `focus` - * and each value is an event handler function. - * - * https://developer.mozilla.org/en-US/docs/Web/API/Element#events - * @typedef {?{ [event: string]: Function }} EventListeners - */ - - /** - * All formatted properties required to render a link, including `tagName`, - * `attributes`, `content` and `eventListeners`. - * @typedef {{ tagName: any, attributes: {[attr: string]: any}, content: string, - * eventListeners: EventListeners }} IntermediateRepresentation - */ - - /** - * Specify either an object described by the template type `O` or a function. - * - * The function takes a string value (usually the link's href attribute), the - * link type (`'url'`, `'hashtag`', etc.) and an internal token representation - * of the link. It should return an object of the template type `O` - * @template O - * @typedef {O | ((value: string, type: string, token: MultiToken) => O)} OptObj - */ - - /** - * Specify either a function described by template type `F` or an object. - * - * Each key in the object should be a link type (`'url'`, `'hashtag`', etc.). Each - * value should be a function with template type `F` that is called when the - * corresponding link type is encountered. - * @template F - * @typedef {F | { [type: string]: F}} OptFn - */ - - /** - * Specify either a value with template type `V`, a function that returns `V` or - * an object where each value resolves to `V`. - * - * The function takes a string value (usually the link's href attribute), the - * link type (`'url'`, `'hashtag`', etc.) and an internal token representation - * of the link. It should return an object of the template type `V` - * - * For the object, each key should be a link type (`'url'`, `'hashtag`', etc.). - * Each value should either have type `V` or a function that returns V. This - * function similarly takes a string value and a token. - * - * Example valid types for `Opt<string>`: - * - * ```js - * 'hello' - * (value, type, token) => 'world' - * { url: 'hello', email: (value, token) => 'world'} - * ``` - * @template V - * @typedef {V | ((value: string, type: string, token: MultiToken) => V) | { [type: string]: V | ((value: string, token: MultiToken) => V) }} Opt - */ - - /** - * See available options: https://linkify.js.org/docs/options.html - * @typedef {{ - * defaultProtocol?: string, - * events?: OptObj<EventListeners>, - * format?: Opt<string>, - * formatHref?: Opt<string>, - * nl2br?: boolean, - * tagName?: Opt<any>, - * target?: Opt<string>, - * rel?: Opt<string>, - * validate?: Opt<boolean>, - * truncate?: Opt<number>, - * className?: Opt<string>, - * attributes?: OptObj<({ [attr: string]: any })>, - * ignoreTags?: string[], - * render?: OptFn<((ir: IntermediateRepresentation) => any)> - * }} Opts - */ - - /** - * @type Required<Opts> - */ - - var defaults = { - defaultProtocol: 'http', - events: null, - format: noop, - formatHref: noop, - nl2br: false, - tagName: 'a', - target: null, - rel: null, - validate: true, - truncate: Infinity, - className: null, - attributes: null, - ignoreTags: [], - render: null - }; - /** - * Utility class for linkify interfaces to apply specified - * {@link Opts formatting and rendering options}. - * - * @param {Opts | Options} [opts] Option value overrides. - * @param {(ir: IntermediateRepresentation) => any} [defaultRender] (For - * internal use) default render function that determines how to generate an - * HTML element based on a link token's derived tagName, attributes and HTML. - * Similar to render option - */ - - function Options(opts, defaultRender) { - if (defaultRender === void 0) { - defaultRender = null; - } - - var o = assign({}, defaults); - - if (opts) { - o = assign(o, opts instanceof Options ? opts.o : opts); - } // Ensure all ignored tags are uppercase - - - var ignoredTags = o.ignoreTags; - var uppercaseIgnoredTags = []; - - for (var i = 0; i < ignoredTags.length; i++) { - uppercaseIgnoredTags.push(ignoredTags[i].toUpperCase()); - } - /** @protected */ - - - this.o = o; - - if (defaultRender) { - this.defaultRender = defaultRender; - } - - this.ignoreTags = uppercaseIgnoredTags; - } - Options.prototype = { - o: defaults, - - /** - * @type string[] - */ - ignoreTags: [], - - /** - * @param {IntermediateRepresentation} ir - * @returns {any} - */ - defaultRender: function defaultRender(ir) { - return ir; - }, - - /** - * Returns true or false based on whether a token should be displayed as a - * link based on the user options. - * @param {MultiToken} token - * @returns {boolean} - */ - check: function check(token) { - return this.get('validate', token.toString(), token); - }, - // Private methods - - /** - * Resolve an option's value based on the value of the option and the given - * params. If operator and token are specified and the target option is - * callable, automatically calls the function with the given argument. - * @template {keyof Opts} K - * @param {K} key Name of option to use - * @param {string} [operator] will be passed to the target option if it's a - * function. If not specified, RAW function value gets returned - * @param {MultiToken} [token] The token from linkify.tokenize - * @returns {Opts[K] | any} - */ - get: function get(key, operator, token) { - var isCallable = operator != null; - var option = this.o[key]; - - if (!option) { - return option; - } - - if (typeof option === 'object') { - option = token.t in option ? option[token.t] : defaults[key]; - - if (typeof option === 'function' && isCallable) { - option = option(operator, token); - } - } else if (typeof option === 'function' && isCallable) { - option = option(operator, token.t, token); - } - - return option; - }, - - /** - * @template {keyof Opts} L - * @param {L} key Name of options object to use - * @param {string} [operator] - * @param {MultiToken} [token] - * @returns {Opts[L] | any} - */ - getObj: function getObj(key, operator, token) { - var obj = this.o[key]; - - if (typeof obj === 'function' && operator != null) { - obj = obj(operator, token.t, token); - } - - return obj; - }, - - /** - * Convert the given token to a rendered element that may be added to the - * calling-interface's DOM - * @param {MultiToken} token Token to render to an HTML element - * @returns {any} Render result; e.g., HTML string, DOM element, React - * Component, etc. - */ - render: function render(token) { - var ir = token.render(this); // intermediate representation - - var renderFn = this.get('render', null, token) || this.defaultRender; - return renderFn(ir, token.t, token); - } - }; - - function noop(val) { - return val; - } - - var options = /*#__PURE__*/Object.freeze({ - __proto__: null, - defaults: defaults, - Options: Options, - assign: assign - }); - - /****************************************************************************** - Multi-Tokens - Tokens composed of arrays of TextTokens - ******************************************************************************/ - - /** - * @param {string} value - * @param {Token[]} tokens - */ - - function MultiToken(value, tokens) { - this.t = 'token'; - this.v = value; - this.tk = tokens; - } - /** - * Abstract class used for manufacturing tokens of text tokens. That is rather - * than the value for a token being a small string of text, it's value an array - * of text tokens. - * - * Used for grouping together URLs, emails, hashtags, and other potential - * creations. - * @class MultiToken - * @property {string} t - * @property {string} v - * @property {Token[]} tk - * @abstract - */ - - MultiToken.prototype = { - isLink: false, - - /** - * Return the string this token represents. - * @return {string} - */ - toString: function toString() { - return this.v; - }, - - /** - * What should the value for this token be in the `href` HTML attribute? - * Returns the `.toString` value by default. - * @param {string} [scheme] - * @return {string} - */ - toHref: function toHref(scheme) { - return this.toString(); - }, - - /** - * @param {Options} options Formatting options - * @returns {string} - */ - toFormattedString: function toFormattedString(options) { - var val = this.toString(); - var truncate = options.get('truncate', val, this); - var formatted = options.get('format', val, this); - return truncate && formatted.length > truncate ? formatted.substring(0, truncate) + '…' : formatted; - }, - - /** - * - * @param {Options} options - * @returns {string} - */ - toFormattedHref: function toFormattedHref(options) { - return options.get('formatHref', this.toHref(options.get('defaultProtocol')), this); - }, - - /** - * The start index of this token in the original input string - * @returns {number} - */ - startIndex: function startIndex() { - return this.tk[0].s; - }, - - /** - * The end index of this token in the original input string (up to this - * index but not including it) - * @returns {number} - */ - endIndex: function endIndex() { - return this.tk[this.tk.length - 1].e; - }, - - /** - Returns an object of relevant values for this token, which includes keys - * type - Kind of token ('url', 'email', etc.) - * value - Original text - * href - The value that should be added to the anchor tag's href - attribute - @method toObject - @param {string} [protocol] `'http'` by default - */ - toObject: function toObject(protocol) { - if (protocol === void 0) { - protocol = defaults.defaultProtocol; - } - - return { - type: this.t, - value: this.toString(), - isLink: this.isLink, - href: this.toHref(protocol), - start: this.startIndex(), - end: this.endIndex() - }; - }, - - /** - * - * @param {Options} options Formatting option - */ - toFormattedObject: function toFormattedObject(options) { - return { - type: this.t, - value: this.toFormattedString(options), - isLink: this.isLink, - href: this.toFormattedHref(options), - start: this.startIndex(), - end: this.endIndex() - }; - }, - - /** - * Whether this token should be rendered as a link according to the given options - * @param {Options} options - * @returns {boolean} - */ - validate: function validate(options) { - return options.get('validate', this.toString(), this); - }, - - /** - * Return an object that represents how this link should be rendered. - * @param {Options} options Formattinng options - */ - render: function render(options) { - var token = this; - var href = this.toFormattedHref(options); - var tagName = options.get('tagName', href, token); - var content = this.toFormattedString(options); - var attributes = {}; - var className = options.get('className', href, token); - var target = options.get('target', href, token); - var rel = options.get('rel', href, token); - var attrs = options.getObj('attributes', href, token); - var eventListeners = options.getObj('events', href, token); - attributes.href = href; - - if (className) { - attributes.class = className; - } - - if (target) { - attributes.target = target; - } - - if (rel) { - attributes.rel = rel; - } - - if (attrs) { - assign(attributes, attrs); - } - - return { - tagName: tagName, - attributes: attributes, - content: content, - eventListeners: eventListeners - }; - } - }; // Base token - /** - * Create a new token that can be emitted by the parser state machine - * @param {string} type readable type of the token - * @param {object} props properties to assign or override, including isLink = true or false - * @returns {new (value: string, tokens: Token[]) => MultiToken} new token class - */ - - function createTokenClass(type, props) { - var Token = /*#__PURE__*/function (_MultiToken) { - _inheritsLoose(Token, _MultiToken); - - function Token(value, tokens) { - var _this; - - _this = _MultiToken.call(this, value, tokens) || this; - _this.t = type; - return _this; - } - - return Token; - }(MultiToken); - - for (var p in props) { - Token.prototype[p] = props[p]; - } - - Token.t = type; - return Token; - } - /** - Represents a list of tokens making up a valid email address - */ - - var Email = createTokenClass('email', { - isLink: true, - toHref: function toHref() { - return 'mailto:' + this.toString(); - } - }); - /** - Represents some plain text - */ - - var Text = createTokenClass('text'); - /** - Multi-linebreak token - represents a line break - @class Nl - */ - - var Nl = createTokenClass('nl'); - /** - Represents a list of text tokens making up a valid URL - @class Url - */ - - var Url = createTokenClass('url', { - isLink: true, - - /** - Lowercases relevant parts of the domain and adds the protocol if - required. Note that this will not escape unsafe HTML characters in the - URL. - @param {string} [scheme] default scheme (e.g., 'https') - @return {string} the full href - */ - toHref: function toHref(scheme) { - if (scheme === void 0) { - scheme = defaults.defaultProtocol; - } - - // Check if already has a prefix scheme - return this.hasProtocol() ? this.v : scheme + "://" + this.v; - }, - - /** - * Check whether this URL token has a protocol - * @return {boolean} - */ - hasProtocol: function hasProtocol() { - var tokens = this.tk; - return tokens.length >= 2 && tokens[0].t !== LOCALHOST && tokens[1].t === COLON; - } - }); - - var multi = /*#__PURE__*/Object.freeze({ - __proto__: null, - MultiToken: MultiToken, - Base: MultiToken, - createTokenClass: createTokenClass, - Email: Email, - Text: Text, - Nl: Nl, - Url: Url - }); - - /** - Not exactly parser, more like the second-stage scanner (although we can - theoretically hotswap the code here with a real parser in the future... but - for a little URL-finding utility abstract syntax trees may be a little - overkill). - - URL format: http://en.wikipedia.org/wiki/URI_scheme - Email format: http://en.wikipedia.org/wiki/EmailAddress (links to RFC in - reference) - - @module linkify - @submodule parser - @main run - */ - - var makeState = function makeState(arg) { - return new State(arg); - }; - /** - * Generate the parser multi token-based state machine - * @param {{ groups: Collections<string> }} tokens - */ - - - function init$1(_ref) { - var groups = _ref.groups; - // Types of characters the URL can definitely end in - var qsAccepting = groups.domain.concat([AMPERSAND, ASTERISK, AT, BACKSLASH, BACKTICK, CARET, DOLLAR, EQUALS, HYPHEN, NUM, PERCENT, PIPE, PLUS, POUND, SLASH, SYM, TILDE, UNDERSCORE]); // Types of tokens that can follow a URL and be part of the query string - // but cannot be the very last characters - // Characters that cannot appear in the URL at all should be excluded - - var qsNonAccepting = [APOSTROPHE, CLOSEANGLEBRACKET, CLOSEBRACE, CLOSEBRACKET, CLOSEPAREN, COLON, COMMA, DOT, EXCLAMATION, OPENANGLEBRACKET, OPENBRACE, OPENBRACKET, OPENPAREN, QUERY, QUOTE, SEMI]; // For addresses without the mailto prefix - // Tokens allowed in the localpart of the email - - var localpartAccepting = [AMPERSAND, APOSTROPHE, ASTERISK, BACKSLASH, BACKTICK, CARET, CLOSEBRACE, DOLLAR, EQUALS, HYPHEN, NUM, OPENBRACE, PERCENT, PIPE, PLUS, POUND, QUERY, SLASH, SYM, TILDE, UNDERSCORE]; // The universal starting state. - - /** - * @type State<Token> - */ - - var Start = makeState(); - var Localpart = tt(Start, TILDE); // Local part of the email address - - ta(Localpart, localpartAccepting, Localpart); - ta(Localpart, groups.domain, Localpart); - var Domain = makeState(), - Scheme = makeState(), - SlashScheme = makeState(); - ta(Start, groups.domain, Domain); // parsed string ends with a potential domain name (A) - - ta(Start, groups.scheme, Scheme); // e.g., 'mailto' - - ta(Start, groups.slashscheme, SlashScheme); // e.g., 'http' - - ta(Domain, localpartAccepting, Localpart); - ta(Domain, groups.domain, Domain); - var LocalpartAt = tt(Domain, AT); // Local part of the email address plus @ - - tt(Localpart, AT, LocalpartAt); // close to an email address now - - var LocalpartDot = tt(Localpart, DOT); // Local part of the email address plus '.' (localpart cannot end in .) - - ta(LocalpartDot, localpartAccepting, Localpart); - ta(LocalpartDot, groups.domain, Localpart); - var EmailDomain = makeState(); - ta(LocalpartAt, groups.domain, EmailDomain); // parsed string starts with local email info + @ with a potential domain name - - ta(EmailDomain, groups.domain, EmailDomain); - var EmailDomainDot = tt(EmailDomain, DOT); // domain followed by DOT - - ta(EmailDomainDot, groups.domain, EmailDomain); - var Email$1 = makeState(Email); // Possible email address (could have more tlds) - - ta(EmailDomainDot, groups.tld, Email$1); - ta(EmailDomainDot, groups.utld, Email$1); - tt(LocalpartAt, LOCALHOST, Email$1); // Hyphen can jump back to a domain name - - var EmailDomainHyphen = tt(EmailDomain, HYPHEN); // parsed string starts with local email info + @ with a potential domain name - - ta(EmailDomainHyphen, groups.domain, EmailDomain); - ta(Email$1, groups.domain, EmailDomain); - tt(Email$1, DOT, EmailDomainDot); - tt(Email$1, HYPHEN, EmailDomainHyphen); // Final possible email states - - var EmailColon = tt(Email$1, COLON); // URL followed by colon (potential port number here) - - /*const EmailColonPort = */ - - ta(EmailColon, groups.numeric, Email); // URL followed by colon and port numner - // Account for dots and hyphens. Hyphens are usually parts of domain names - // (but not TLDs) - - var DomainHyphen = tt(Domain, HYPHEN); // domain followed by hyphen - - var DomainDot = tt(Domain, DOT); // domain followed by DOT - - ta(DomainHyphen, groups.domain, Domain); - ta(DomainDot, localpartAccepting, Localpart); - ta(DomainDot, groups.domain, Domain); - var DomainDotTld = makeState(Url); // Simplest possible URL with no query string - - ta(DomainDot, groups.tld, DomainDotTld); - ta(DomainDot, groups.utld, DomainDotTld); - ta(DomainDotTld, groups.domain, Domain); - ta(DomainDotTld, localpartAccepting, Localpart); - tt(DomainDotTld, DOT, DomainDot); - tt(DomainDotTld, HYPHEN, DomainHyphen); - tt(DomainDotTld, AT, LocalpartAt); - var DomainDotTldColon = tt(DomainDotTld, COLON); // URL followed by colon (potential port number here) - - var DomainDotTldColonPort = makeState(Url); // TLD followed by a port number - - ta(DomainDotTldColon, groups.numeric, DomainDotTldColonPort); // Long URL with optional port and maybe query string - - var Url$1 = makeState(Url); // URL with extra symbols at the end, followed by an opening bracket - - var UrlNonaccept = makeState(); // URL followed by some symbols (will not be part of the final URL) - // Query strings - - ta(Url$1, qsAccepting, Url$1); - ta(Url$1, qsNonAccepting, UrlNonaccept); - ta(UrlNonaccept, qsAccepting, Url$1); - ta(UrlNonaccept, qsNonAccepting, UrlNonaccept); // Become real URLs after `SLASH` or `COLON NUM SLASH` - // Here works with or without scheme:// prefix - - tt(DomainDotTld, SLASH, Url$1); - tt(DomainDotTldColonPort, SLASH, Url$1); // Note that domains that begin with schemes are treated slighly differently - - var UriPrefix = tt(Scheme, COLON); // e.g., 'mailto:' or 'http://' - - var SlashSchemeColon = tt(SlashScheme, COLON); // e.g., 'http:' - - var SlashSchemeColonSlash = tt(SlashSchemeColon, SLASH); // e.g., 'http:/' - - tt(SlashSchemeColonSlash, SLASH, UriPrefix); // Scheme states can transition to domain states - - ta(Scheme, groups.domain, Domain); - tt(Scheme, DOT, DomainDot); - tt(Scheme, HYPHEN, DomainHyphen); - ta(SlashScheme, groups.domain, Domain); - tt(SlashScheme, DOT, DomainDot); - tt(SlashScheme, HYPHEN, DomainHyphen); // Force URL with scheme prefix followed by anything sane - - ta(UriPrefix, groups.domain, Url$1); - tt(UriPrefix, SLASH, Url$1); // URL, followed by an opening bracket - - var UrlOpenbrace = tt(Url$1, OPENBRACE); // URL followed by { - - var UrlOpenbracket = tt(Url$1, OPENBRACKET); // URL followed by [ - - var UrlOpenanglebracket = tt(Url$1, OPENANGLEBRACKET); // URL followed by < - - var UrlOpenparen = tt(Url$1, OPENPAREN); // URL followed by ( - - tt(UrlNonaccept, OPENBRACE, UrlOpenbrace); - tt(UrlNonaccept, OPENBRACKET, UrlOpenbracket); - tt(UrlNonaccept, OPENANGLEBRACKET, UrlOpenanglebracket); - tt(UrlNonaccept, OPENPAREN, UrlOpenparen); // Closing bracket component. This character WILL be included in the URL - - tt(UrlOpenbrace, CLOSEBRACE, Url$1); - tt(UrlOpenbracket, CLOSEBRACKET, Url$1); - tt(UrlOpenanglebracket, CLOSEANGLEBRACKET, Url$1); - tt(UrlOpenparen, CLOSEPAREN, Url$1); - tt(UrlOpenbrace, CLOSEBRACE, Url$1); // URL that beings with an opening bracket, followed by a symbols. - // Note that the final state can still be `UrlOpenbrace` (if the URL only - // has a single opening bracket for some reason). - - var UrlOpenbraceQ = makeState(Url); // URL followed by { and some symbols that the URL can end it - - var UrlOpenbracketQ = makeState(Url); // URL followed by [ and some symbols that the URL can end it - - var UrlOpenanglebracketQ = makeState(Url); // URL followed by < and some symbols that the URL can end it - - var UrlOpenparenQ = makeState(Url); // URL followed by ( and some symbols that the URL can end it - - ta(UrlOpenbrace, qsAccepting, UrlOpenbraceQ); - ta(UrlOpenbracket, qsAccepting, UrlOpenbracketQ); - ta(UrlOpenanglebracket, qsAccepting, UrlOpenanglebracketQ); - ta(UrlOpenparen, qsAccepting, UrlOpenparenQ); - var UrlOpenbraceSyms = makeState(); // UrlOpenbrace followed by some symbols it cannot end it - - var UrlOpenbracketSyms = makeState(); // UrlOpenbracketQ followed by some symbols it cannot end it - - var UrlOpenanglebracketSyms = makeState(); // UrlOpenanglebracketQ followed by some symbols it cannot end it - - var UrlOpenparenSyms = makeState(); // UrlOpenparenQ followed by some symbols it cannot end it - - ta(UrlOpenbrace, qsNonAccepting); - ta(UrlOpenbracket, qsNonAccepting); - ta(UrlOpenanglebracket, qsNonAccepting); - ta(UrlOpenparen, qsNonAccepting); // URL that begins with an opening bracket, followed by some symbols - - ta(UrlOpenbraceQ, qsAccepting, UrlOpenbraceQ); - ta(UrlOpenbracketQ, qsAccepting, UrlOpenbracketQ); - ta(UrlOpenanglebracketQ, qsAccepting, UrlOpenanglebracketQ); - ta(UrlOpenparenQ, qsAccepting, UrlOpenparenQ); - ta(UrlOpenbraceQ, qsNonAccepting, UrlOpenbraceQ); - ta(UrlOpenbracketQ, qsNonAccepting, UrlOpenbracketQ); - ta(UrlOpenanglebracketQ, qsNonAccepting, UrlOpenanglebracketQ); - ta(UrlOpenparenQ, qsNonAccepting, UrlOpenparenQ); - ta(UrlOpenbraceSyms, qsAccepting, UrlOpenbraceSyms); - ta(UrlOpenbracketSyms, qsAccepting, UrlOpenbracketQ); - ta(UrlOpenanglebracketSyms, qsAccepting, UrlOpenanglebracketQ); - ta(UrlOpenparenSyms, qsAccepting, UrlOpenparenQ); - ta(UrlOpenbraceSyms, qsNonAccepting, UrlOpenbraceSyms); - ta(UrlOpenbracketSyms, qsNonAccepting, UrlOpenbracketSyms); - ta(UrlOpenanglebracketSyms, qsNonAccepting, UrlOpenanglebracketSyms); - ta(UrlOpenparenSyms, qsNonAccepting, UrlOpenparenSyms); // Close brace/bracket to become regular URL - - tt(UrlOpenbracketQ, CLOSEBRACKET, Url$1); - tt(UrlOpenanglebracketQ, CLOSEANGLEBRACKET, Url$1); - tt(UrlOpenparenQ, CLOSEPAREN, Url$1); - tt(UrlOpenbraceQ, CLOSEBRACE, Url$1); - tt(UrlOpenbracketSyms, CLOSEBRACKET, Url$1); - tt(UrlOpenanglebracketSyms, CLOSEANGLEBRACKET, Url$1); - tt(UrlOpenparenSyms, CLOSEPAREN, Url$1); - tt(UrlOpenbraceSyms, CLOSEPAREN, Url$1); - tt(Start, LOCALHOST, DomainDotTld); // localhost is a valid URL state - - tt(Start, NL$1, Nl); // single new line - - return { - start: Start, - tokens: tk - }; - } - /** - * Run the parser state machine on a list of scanned string-based tokens to - * create a list of multi tokens, each of which represents a URL, email address, - * plain text, etc. - * - * @param {State<MultiToken>} start parser start state - * @param {string} input the original input used to generate the given tokens - * @param {Token[]} tokens list of scanned tokens - * @returns {MultiToken[]} - */ - - function run(start, input, tokens) { - var len = tokens.length; - var cursor = 0; - var multis = []; - var textTokens = []; - - while (cursor < len) { - var state = start; - var secondState = null; - var nextState = null; - var multiLength = 0; - var latestAccepting = null; - var sinceAccepts = -1; - - while (cursor < len && !(secondState = state.go(tokens[cursor].t))) { - // Starting tokens with nowhere to jump to. - // Consider these to be just plain text - textTokens.push(tokens[cursor++]); - } - - while (cursor < len && (nextState = secondState || state.go(tokens[cursor].t))) { - // Get the next state - secondState = null; - state = nextState; // Keep track of the latest accepting state - - if (state.accepts()) { - sinceAccepts = 0; - latestAccepting = state; - } else if (sinceAccepts >= 0) { - sinceAccepts++; - } - - cursor++; - multiLength++; - } - - if (sinceAccepts < 0) { - // No accepting state was found, part of a regular text token add - // the first text token to the text tokens array and try again from - // the next - cursor -= multiLength; - - if (cursor < len) { - textTokens.push(tokens[cursor]); - cursor++; - } - } else { - // Accepting state! - // First close off the textTokens (if available) - if (textTokens.length > 0) { - multis.push(initMultiToken(Text, input, textTokens)); - textTokens = []; - } // Roll back to the latest accepting state - - - cursor -= sinceAccepts; - multiLength -= sinceAccepts; // Create a new multitoken - - var Multi = latestAccepting.t; - var subtokens = tokens.slice(cursor - multiLength, cursor); - multis.push(initMultiToken(Multi, input, subtokens)); - } - } // Finally close off the textTokens (if available) - - - if (textTokens.length > 0) { - multis.push(initMultiToken(Text, input, textTokens)); - } - - return multis; - } - /** - * Utility function for instantiating a new multitoken with all the relevant - * fields during parsing. - * @param {new (value: string, tokens: Token[]) => MultiToken} Multi class to instantiate - * @param {string} input original input string - * @param {Token[]} tokens consecutive tokens scanned from input string - * @returns {MultiToken} - */ - - function initMultiToken(Multi, input, tokens) { - var startIdx = tokens[0].s; - var endIdx = tokens[tokens.length - 1].e; - var value = input.slice(startIdx, endIdx); - return new Multi(value, tokens); - } - - var warn = typeof console !== 'undefined' && console && console.warn || function () {}; - - var warnAdvice = 'To avoid this warning, please register all custom schemes before invoking linkify the first time.'; // Side-effect initialization state - - var INIT = { - scanner: null, - parser: null, - tokenQueue: [], - pluginQueue: [], - customSchemes: [], - initialized: false - }; - /** - * @typedef {{ - * start: State<string>, - * tokens: { groups: Collections<string> } & typeof tk - * }} ScannerInit - */ - - /** - * @typedef {{ - * start: State<MultiToken>, - * tokens: typeof multi - * }} ParserInit - */ - - /** - * @typedef {(arg: { scanner: ScannerInit }) => void} TokenPlugin - */ - - /** - * @typedef {(arg: { scanner: ScannerInit, parser: ParserInit }) => void} Plugin - */ - - /** - * De-register all plugins and reset the internal state-machine. Used for - * testing; not required in practice. - * @private - */ - - function reset() { - State.groups = {}; - INIT.scanner = null; - INIT.parser = null; - INIT.tokenQueue = []; - INIT.pluginQueue = []; - INIT.customSchemes = []; - INIT.initialized = false; - } - /** - * Register a token plugin to allow the scanner to recognize additional token - * types before the parser state machine is constructed from the results. - * @param {string} name of plugin to register - * @param {TokenPlugin} plugin function that accepts the scanner state machine - * and available scanner tokens and collections and extends the state machine to - * recognize additional tokens or groups. - */ - - function registerTokenPlugin(name, plugin) { - if (typeof plugin !== 'function') { - throw new Error("linkifyjs: Invalid token plugin " + plugin + " (expects function)"); - } - - for (var i = 0; i < INIT.tokenQueue.length; i++) { - if (name === INIT.tokenQueue[i][0]) { - warn("linkifyjs: token plugin \"" + name + "\" already registered - will be overwritten"); - INIT.tokenQueue[i] = [name, plugin]; - return; - } - } - - INIT.tokenQueue.push([name, plugin]); - - if (INIT.initialized) { - warn("linkifyjs: already initialized - will not register token plugin \"" + name + "\" until you manually call linkify.init(). " + warnAdvice); - } - } - /** - * Register a linkify plugin - * @param {string} name of plugin to register - * @param {Plugin} plugin function that accepts the parser state machine and - * extends the parser to recognize additional link types - */ - - function registerPlugin(name, plugin) { - if (typeof plugin !== 'function') { - throw new Error("linkifyjs: Invalid plugin " + plugin + " (expects function)"); - } - - for (var i = 0; i < INIT.pluginQueue.length; i++) { - if (name === INIT.pluginQueue[i][0]) { - warn("linkifyjs: plugin \"" + name + "\" already registered - will be overwritten"); - INIT.pluginQueue[i] = [name, plugin]; - return; - } - } - - INIT.pluginQueue.push([name, plugin]); - - if (INIT.initialized) { - warn("linkifyjs: already initialized - will not register plugin \"" + name + "\" until you manually call linkify.init(). " + warnAdvice); - } - } - /** - * Detect URLs with the following additional protocol. Anything with format - * "protocol://..." will be considered a link. If `optionalSlashSlash` is set to - * `true`, anything with format "protocol:..." will be considered a link. - * @param {string} protocol - * @param {boolean} [optionalSlashSlash] - */ - - function registerCustomProtocol(scheme, optionalSlashSlash) { - if (optionalSlashSlash === void 0) { - optionalSlashSlash = false; - } - - if (INIT.initialized) { - warn("linkifyjs: already initialized - will not register custom scheme \"" + scheme + "\" until you manually call linkify.init(). " + warnAdvice); - } - - if (!/^[0-9a-z]+(-[0-9a-z]+)*$/.test(scheme)) { - throw new Error('linkifyjs: incorrect scheme format.\n 1. Must only contain digits, lowercase ASCII letters or "-"\n 2. Cannot start or end with "-"\n 3. "-" cannot repeat'); - } - - INIT.customSchemes.push([scheme, optionalSlashSlash]); - } - /** - * Initialize the linkify state machine. Called automatically the first time - * linkify is called on a string, but may be called manually as well. - */ - - function init() { - // Initialize scanner state machine and plugins - INIT.scanner = init$2(INIT.customSchemes); - - for (var i = 0; i < INIT.tokenQueue.length; i++) { - INIT.tokenQueue[i][1]({ - scanner: INIT.scanner - }); - } // Initialize parser state machine and plugins - - - INIT.parser = init$1(INIT.scanner.tokens); - - for (var _i = 0; _i < INIT.pluginQueue.length; _i++) { - INIT.pluginQueue[_i][1]({ - scanner: INIT.scanner, - parser: INIT.parser - }); - } - - INIT.initialized = true; - } - /** - * Parse a string into tokens that represent linkable and non-linkable sub-components - * @param {string} str - * @return {MultiToken[]} tokens - */ - - function tokenize(str) { - if (!INIT.initialized) { - init(); - } - - return run(INIT.parser.start, str, run$1(INIT.scanner.start, str)); - } - /** - * Find a list of linkable items in the given string. - * @param {string} str string to find links in - * @param {string | Opts} [type] either formatting options or specific type of - * links to find, e.g., 'url' or 'email' - * @param {Opts} [opts] formatting options for final output. Cannot be specified - * if opts already provided in `type` argument - */ - - function find(str, type, opts) { - if (type === void 0) { - type = null; - } - - if (opts === void 0) { - opts = null; - } - - if (type && typeof type === 'object') { - if (opts) { - throw Error("linkifyjs: Invalid link type " + type + "; must be a string"); - } - - opts = type; - type = null; - } - - var options = new Options(opts); - var tokens = tokenize(str); - var filtered = []; - - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - if (token.isLink && (!type || token.t === type)) { - filtered.push(token.toFormattedObject(options)); - } - } - - return filtered; - } - /** - * Is the given string valid linkable text of some sort. Note that this does not - * trim the text for you. - * - * Optionally pass in a second `type` param, which is the type of link to test - * for. - * - * For example, - * - * linkify.test(str, 'email'); - * - * Returns `true` if str is a valid email. - * @param {string} str string to test for links - * @param {string} [type] optional specific link type to look for - * @returns boolean true/false - */ - - function test(str, type) { - if (type === void 0) { - type = null; - } - - var tokens = tokenize(str); - return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].t === type); - } - - exports.MultiToken = MultiToken; - exports.Options = Options; - exports.State = State; - exports.createTokenClass = createTokenClass; - exports.find = find; - exports.init = init; - exports.multi = multi; - exports.options = options; - exports.regexp = regexp; - exports.registerCustomProtocol = registerCustomProtocol; - exports.registerPlugin = registerPlugin; - exports.registerTokenPlugin = registerTokenPlugin; - exports.reset = reset; - exports.stringToArray = stringToArray; - exports.test = test; - exports.tokenize = tokenize; - - Object.defineProperty(exports, '__esModule', { value: true }); - - return exports; - -})({}); \ No newline at end of file diff --git a/resources/webengine/previewInfo.js b/resources/webengine/previewInfo.js deleted file mode 100644 index 83a14cae92775ce08436f8c09b6420bb2ec82a33..0000000000000000000000000000000000000000 --- a/resources/webengine/previewInfo.js +++ /dev/null @@ -1,126 +0,0 @@ -/* MIT License - -Copyright (c) 2019 Andrej Gajdos - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.*/ - -/** - * Retrieves the title of a webpage which is used to fill out the preview of a hyperlink - * @param doc the DOM of the url that is being previewed - * @returns the title of the given webpage - */ - -function getTitle(doc){ - const og_title = doc.querySelector('meta[property="og:title"]') - if (og_title !== null && og_title.content.length > 0) { - return og_title.content - } - const twitter_title = doc.querySelector('meta[name="twitter:title"]') - if (twitter_title !== null && twitter_title.content.length > 0) { - return twitter_title.content - } - const doc_title = doc.title - if (doc_title !== null && doc_title.length > 0) { - return doc_title - } - if (doc.querySelector("h1") !== null){ - const header_1 = doc.querySelector("h1").innerHTML - if (header_1 !== null && header_1.length > 0) { - return header_1 - } - } - if (doc.querySelector("h2") !== null){ - const header_2 = doc.querySelector("h2").innerHTML - if (header_2 !== null && header_2.length > 0) { - return header_2 - } - } - return null -} - -/** - * Obtains a description of the webpage for the hyperlink preview - * @param doc the DOM of the url that is being previewed - * @returns a description of the webpage - */ -function getDescription(doc){ - const og_description = doc.querySelector('meta[property="og:description"]') - if (og_description !== null && og_description.content.length > 0) { - return og_description.content - } - const twitter_description = doc.querySelector('meta[name="twitter:description"]') - if (twitter_description !== null && twitter_description.content.length > 0) { - return twitter_description.content - } - const meta_description = doc.querySelector('meta[name="description"]') - if (meta_description !== null && meta_description.content.length > 0) { - return meta_description.content - } - var all_paragraphs = doc.querySelectorAll("p") - let first_visible_paragraph = null - for (var i = 0; i < all_paragraphs.length; i++) { - if (all_paragraphs[i].offsetParent !== null && - !all_paragraphs[i].childElementCount !== 0) { - first_visible_paragraph = all_paragraphs[i].textContent - break - } - } - return first_visible_paragraph -} - -/** - * Gets the image that represents a webpage. - * @param doc the DOM of the url that is being previewed - * @returns the image representing the url or null if no such image was found - */ -function getImage(doc) { - const og_image = doc.querySelector('meta[property="og:image"]') - if (og_image !== null && og_image.content.length > 0){ - return og_image.content - } - const image_rel_link = doc.querySelector('link[rel="image_src"]') - if (image_rel_link !== null && image_rel_link.href.length > 0){ - return image_rel_link.href - } - const twitter_img = doc.querySelector('meta[name="twitter:image"]') - if (twitter_img !== null && twitter_img.content.length > 0) { - return twitter_img.content - } - - let imgs = Array.from(doc.getElementsByTagName("img")) - if (imgs.length > 0) { - imgs = imgs.filter(img => { - let add_image = true - if (img.naturalWidth > img.naturalHeight) { - if (img.naturalWidth / img.naturalHeight > 3) { - add_image = false - } - } else { - if (img.naturalHeight / img.naturalWidth > 3) { - add_image = false - } - } - if (img.naturalHeight <= 50 || img.naturalWidth <= 50) { - add_image = false - } - return add_image - }) - } - return null -} diff --git a/resources/webengine/previewInterop.js b/resources/webengine/previewInterop.js deleted file mode 100644 index 6e4709bf486b59e90495493f01c82c195b07a9e5..0000000000000000000000000000000000000000 --- a/resources/webengine/previewInterop.js +++ /dev/null @@ -1,93 +0,0 @@ -_ = new QWebChannel(qt.webChannelTransport, function (channel) { - window.jsbridge = channel.objects.jsbridge -}) - -function log(msg) { - window.jsbridge.log(msg) -} - -function getPreviewInfo(messageId, url) { - var title = null - var description = null - var image = null - var u = new URL(url) - if (u.protocol === '') { - url = "https://".concat(url) - } - var domain = (new URL(url)) - fetch(url, { - mode: 'no-cors', - headers: {'Set-Cookie': 'SameSite=None; Secure'} - }).then(function (response) { - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('text/html')) { - return - } - return response.body - }).then(body => { - const reader = body.getReader(); - - return new ReadableStream({ - start(controller) { - return pump(); - - function pump() { - return reader.read().then(({ done, value }) => { - // When no more data needs to be consumed, close the stream - if (done) { - controller.close(); - return; - } - if(value.byteLength > 2*1024*1024) { - controller.close(); - return; - } - - // Enqueue the next data chunk into our target stream - controller.enqueue(value); - return pump(); - }); - } - } - }) - }, e => Promise.reject(e)) - .then(stream => new Response(stream)) - .then(response => response.text()) - .then(function (html) { - // create DOM from html string - var parser = new DOMParser() - var doc = parser.parseFromString(html, "text/html") - if (!url.includes("twitter.com")){ - title = getTitle(doc) - } else { - title = "Twitter. It's what's happening." - } - image = getImage(doc, url) - description = getDescription(doc) - domain = (domain.hostname).replace("www.", "") - }).catch(function (err) { - log("Error occured while fetching document: " + err) - }).finally(() => { - window.jsbridge.emitInfoReady(messageId, { - 'title': title, - 'image': image, - 'description': description, - 'url': url, - 'domain': domain, - }) - }) -} - -function parseMessage(messageId, message, showPreview, color='#0645AD') { - var links = linkify.find(message) - if (links.length === 0) { - return - } - if (showPreview) - getPreviewInfo(messageId, links[0].href) - window.jsbridge.emitLinkified(messageId, linkifyStr(message, { - attributes: { - style: "color:" + color + ";" - } - })) -} diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml index e657b310370ba96123984d293f6600d02514b635..44935cfeacfe52210dccf5d5dfc5468ad90c3b4e 100644 --- a/src/app/MainApplicationWindow.qml +++ b/src/app/MainApplicationWindow.qml @@ -329,15 +329,15 @@ ApplicationWindow { function onUpdateErrorOccurred(error) { presentUpdateInfoDialog((function () { switch (error) { - case NetWorkManager.ACCESS_DENIED: + case NetworkManager.ACCESS_DENIED: return JamiStrings.genericError; - case NetWorkManager.DISCONNECTED: + case NetworkManager.DISCONNECTED: return JamiStrings.networkDisconnected; - case NetWorkManager.NETWORK_ERROR: + case NetworkManager.NETWORK_ERROR: return JamiStrings.updateNetworkError; - case NetWorkManager.SSL_ERROR: + case NetworkManager.SSL_ERROR: return JamiStrings.updateSSLError; - case NetWorkManager.CANCELED: + case NetworkManager.CANCELED: return JamiStrings.updateDownloadCanceled; default: return {}; diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml index b46d430693f84d8d64c1518c0630fe1305a17d97..978f8f82976cd8bde60f00244ad2d32f3d857ed5 100644 --- a/src/app/commoncomponents/TextMessageDelegate.qml +++ b/src/app/commoncomponents/TextMessageDelegate.qml @@ -51,10 +51,11 @@ SBSMessageBase { padding: isEmojiOnly ? 0 : JamiTheme.preferredMarginSize anchors.right: isOutgoing ? parent.right : undefined text: { - if (LinkifiedBody !== "" && Linkified.length === 0) { - MessagesAdapter.parseMessageUrls(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl); + if (Body !== "" && ParsedBody.length === 0) { + MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl, CurrentConversation.color); + return "" } - return (LinkifiedBody !== "") ? LinkifiedBody : "*(" + JamiStrings.deletedMessage + ")*"; + return (ParsedBody !== "") ? ParsedBody : "*(" + JamiStrings.deletedMessage + ")*"; } horizontalAlignment: Text.AlignLeft @@ -76,7 +77,8 @@ SBSMessageBase { font.pixelSize: isEmojiOnly ? JamiTheme.chatviewEmojiSize : JamiTheme.emojiBubbleSize font.hintingPreference: Font.PreferNoHinting renderType: Text.NativeRendering - textFormat: Text.MarkdownText + textFormat: Text.RichText + clip: true onLinkHovered: root.hoveredLink = hoveredLink onLinkActivated: Qt.openUrlExternally(new URL(hoveredLink)) readOnly: true @@ -227,7 +229,7 @@ SBSMessageBase { renderType: Text.NativeRendering textFormat: TextEdit.RichText color: UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark - visible: LinkPreviewInfo.title !== null + visible: LinkPreviewInfo.title.length > 0 text: LinkPreviewInfo.title } Label { @@ -237,7 +239,7 @@ SBSMessageBase { wrapMode: Label.WrapAtWordBoundaryOrAnywhere renderType: Text.NativeRendering textFormat: TextEdit.RichText - visible: LinkPreviewInfo.description !== null + visible: LinkPreviewInfo.description.length > 0 font.underline: root.hoveredLink text: LinkPreviewInfo.description color: root.colorUrl @@ -263,10 +265,5 @@ SBSMessageBase { duration: 100 } } - Component.onCompleted: { - if (Linkified.length === 0) { - MessagesAdapter.parseMessageUrls(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl); - } - opacity = 1; - } + Component.onCompleted: opacity = 1; } diff --git a/src/app/connectivitymonitor.cpp b/src/app/connectivitymonitor.cpp index f3b04559fc49d90b9b0bfc45ab61857bae0b331b..b622a892ad237e6ee75fd932b5c7db207b1fb47f 100644 --- a/src/app/connectivitymonitor.cpp +++ b/src/app/connectivitymonitor.cpp @@ -183,7 +183,7 @@ primaryConnectionChanged(NMClient* nm, GParamSpec*, ConnectivityMonitor* cm) { auto connection = nm_client_get_primary_connection(nm); logConnectionInfo(connection); - cm->connectivityChanged(); + Q_EMIT cm->connectivityChanged(); } static void diff --git a/src/app/connectivitymonitor.h b/src/app/connectivitymonitor.h index 9e2ac65dfa2775db278181212331b51f078eadf1..e74a22c7c214ddc0370c8cd8cf8e41ad9276d9a8 100644 --- a/src/app/connectivitymonitor.h +++ b/src/app/connectivitymonitor.h @@ -20,7 +20,6 @@ #include <QObject> -#ifdef Q_OS_WIN class ConnectivityMonitor final : public QObject { Q_OBJECT @@ -34,6 +33,7 @@ Q_SIGNALS: void connectivityChanged(); private: +#ifdef Q_OS_WIN void destroy(); struct INetworkListManager* pNetworkListManager_; @@ -41,21 +41,5 @@ private: struct IConnectionPoint* pConnectPoint_; class NetworkEventHandler* netEventHandler_; unsigned long cookie_; -}; - -#else -// TODO: platform implementations should be in the daemon. - -class ConnectivityMonitor final : public QObject -{ - Q_OBJECT -public: - explicit ConnectivityMonitor(QObject* parent = nullptr); - ~ConnectivityMonitor(); - - bool isOnline(); - -Q_SIGNALS: - void connectivityChanged(); -}; #endif // Q_OS_WIN +}; diff --git a/src/app/htmlparser.h b/src/app/htmlparser.h new file mode 100644 index 0000000000000000000000000000000000000000..c656542b9a8937f718373a9ad63ba026b43d5e8a --- /dev/null +++ b/src/app/htmlparser.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QObject> +#include <QVariantMap> + +#include "tidy.h" +#include "tidybuffio.h" + +// This class is used to parse HTML strings. It uses the libtidy library to parse +// the HTML and traverse the DOM tree. It can be used to extract a list of tags +// and their values from an HTML string. +// Currently, it is used to extract the <a> and <pre> tags from a message body, +// and in the future it can be used in conjunction with QtNetwork to generate link +// previews without having to use QtWebEngine. +class HtmlParser : public QObject +{ + Q_OBJECT +public: + HtmlParser(QObject* parent = nullptr) + : QObject(parent) + { + doc_ = tidyCreate(); + tidyOptSetBool(doc_, TidyQuiet, yes); + tidyOptSetBool(doc_, TidyShowWarnings, no); + } + + ~HtmlParser() + { + tidyRelease(doc_); + } + + bool parseHtmlString(const QString& html) + { + return tidyParseString(doc_, html.toLocal8Bit().data()) >= 0; + } + + using TagInfoList = QMap<TidyTagId, QList<QString>>; + + // A function that traverses the DOM tree and fills a QVariantMap with a list + // of the tags and their values. The result is structured as follows: + // {tagId1: ["tagValue1", "tagValue2", ...], + // tagId: ["tagValue1", "tagValue2", ...], + // ... } + TagInfoList getTags(QList<TidyTagId> tags, int maxDepth = -1) + { + TagInfoList result; + traverseNode( + tidyGetRoot(doc_), + tags, + [&result](const QString& value, TidyTagId tag) { result[tag].append(value); }, + maxDepth); + + return result; + } + + QString getFirstTagValue(TidyTagId tag, int maxDepth = -1) + { + QString result; + traverseNode( + tidyGetRoot(doc_), + {tag}, + [&result](const QString& value, TidyTagId) { result = value; }, + maxDepth); + return result; + } + +private: + void traverseNode(TidyNode node, + QList<TidyTagId> tags, + const std::function<void(const QString&, TidyTagId)>& cb, + int depth = -1) + { + TidyBuffer nodeValue = {}; + for (auto tag : tags) { + if (tidyNodeGetId(node) == tag && tidyNodeGetText(doc_, node, &nodeValue) == yes && cb) { + cb(QString::fromLocal8Bit(nodeValue.bp), tag); + if (depth != -1 && --depth == 0) { + return; + } + } + } + + // Traverse the children of the current node. + for (TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) { + traverseNode(child, tags, cb, depth); + } + } + + TidyDoc doc_; +}; diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index b310cfbb8205bc2a6aad76bcb7fbd4839534dbb8..9ed5eff04578224dfe5db6873e36df88955fae9d 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -122,7 +122,7 @@ MainApplication::init() connectivityMonitor_.reset(new ConnectivityMonitor(this)); settingsManager_.reset(new AppSettingsManager(this)); systemTray_.reset(new SystemTray(settingsManager_.get(), this)); - previewEngine_.reset(new PreviewEngine(this)); + previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this)); QObject::connect(settingsManager_.get(), &AppSettingsManager::retranslate, diff --git a/src/app/mainview/components/EditContainer.qml b/src/app/mainview/components/EditContainer.qml index 43b80df1a077911632cb72f3201950fa8e98fea3..2c78dafd1c1a739d4ba2dad46d076cafda18c35b 100644 --- a/src/app/mainview/components/EditContainer.qml +++ b/src/app/mainview/components/EditContainer.qml @@ -31,7 +31,7 @@ Rectangle { property var body: { if (MessagesAdapter.editId === "") return ""; - return MessagesAdapter.dataForInteraction(MessagesAdapter.editId, MessageList.LinkifiedBody); + return MessagesAdapter.dataForInteraction(MessagesAdapter.editId, MessageList.ParsedBody); } RowLayout { diff --git a/src/app/messageparser.cpp b/src/app/messageparser.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7941e12dbefc5ca424eef323abdd64bf1939c479 --- /dev/null +++ b/src/app/messageparser.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "messageparser.h" + +#include "previewengine.h" +#include "htmlparser.h" + +#include <QRegularExpression> +#include <QtConcurrent> + +#include "md4c-html.h" + +MessageParser::MessageParser(PreviewEngine* previewEngine, QObject* parent) + : QObject(parent) + , previewEngine_(previewEngine) + , htmlParser_(new HtmlParser(this)) + , threadPool_(new QThreadPool(this)) +{ + threadPool_->setMaxThreadCount(1); + connect(previewEngine_, &PreviewEngine::infoReady, this, &MessageParser::linkInfoReady); +} + +void +MessageParser::parseMessage(const QString& messageId, + const QString& msg, + bool previewLinks, + const QColor& linkColor, + const QColor& backgroundColor) +{ + // Run everything here on a separate thread. + threadPool_->start( + [this, messageId, md = msg, previewLinks, linkColor, backgroundColor]() mutable -> void { + preprocessMarkdown(md); + auto html = markdownToHtml(md.toUtf8().constData()); + + // Now that we have the HTML, we can parse it to get a list of tags and their values. + // We are only interested in the <a> and <pre> tags. + htmlParser_->parseHtmlString(html); + auto tagsMap = htmlParser_->getTags({TidyTag_A, TidyTag_DEL, TidyTag_PRE}); + + static QString styleTag("<style>%1</style>"); + QString style; + + // Check for any <pre> tags. If there are any, we need to: + // 1. add some CSS to color them. + // 2. add some CSS to make them wrap. + if (tagsMap.contains(TidyTag_PRE)) { + auto textColor = (0.2126 * backgroundColor.red() + 0.7152 * backgroundColor.green() + + 0.0722 * backgroundColor.blue()) + < 153 + ? QColor(255, 255, 255) + : QColor(0, 0, 0); + style += QString("pre,code{background-color:%1;" + "color:%2;white-space:pre-wrap;}") + .arg(backgroundColor.name(), textColor.name()); + } + + // md4c makes DEL tags instead of S tags for ~~strikethrough-text~~, + // so we need to style the DEL tag content. + if (tagsMap.contains(TidyTag_DEL)) { + style += QString("del{text-decoration:line-through;}"); + } + + // Check for any <a> tags. If there are any, we need to: + // 1. add some CSS to color them. + // 2. parse them to get a preview IF the user has enabled link previews. + if (tagsMap.contains(TidyTag_A)) { + style += QString("a{color:%1;}").arg(linkColor.name()); + + // Update the UI before we start parsing the link. + html.prepend(QString(styleTag).arg(style)); + Q_EMIT messageParsed(messageId, html); + + // If the user has enabled link previews, then we need to generate the link preview. + if (previewLinks) { + // Get the first link in the message. + auto anchorTag = tagsMap[TidyTag_A].first(); + static QRegularExpression hrefRegex("href=\"(.*?)\""); + auto match = hrefRegex.match(anchorTag); + if (match.hasMatch()) { + Q_EMIT previewEngine_->parseLink(messageId, match.captured(1)); + } + } + + return; + } + + // If the message didn't contain any links, then we can just update the UI. + html.prepend(QString(styleTag).arg(style)); + Q_EMIT messageParsed(messageId, html); + }); +} + +void +MessageParser::preprocessMarkdown(QString& markdown) +{ + // Match all instances of the linefeed character. + static QRegularExpression newlineRegex("\n"); + static const QString newline = " \n"; + + // Replace all instances of the linefeed character with 2 spaces + a linefeed character + // in order to force a line break in the HTML. + // Note: we should only do this for non-code fenced blocks. + static QRegularExpression codeFenceRe("`{1,3}([\\s\\S]*?)`{1,3}"); + auto match = codeFenceRe.globalMatch(markdown); + + // If there are no code blocks, then we can just replace all linefeeds with 2 spaces + // + a linefeed, and we're done. + if (!match.hasNext()) { + markdown.replace(newlineRegex, newline); + return; + } + + // Save each block of text and code. The text blocks will be + // processed for line breaks and the code blocks will be left + // as is. + enum BlockType { Text, Code }; + QVector<QPair<BlockType, QString>> codeBlocks; + + int start = 0; + while (match.hasNext()) { + auto m = match.next(); + auto nonCodelength = m.capturedStart() - start; + if (nonCodelength) { + codeBlocks.push_back({Text, markdown.mid(start, nonCodelength)}); + } + codeBlocks.push_back({Code, m.captured(0)}); + start = m.capturedStart() + m.capturedLength(); + } + // There may be some text after the last code block. + if (start < markdown.size()) { + codeBlocks.push_back({Text, markdown.mid(start)}); + } + + // Now we can process the text blocks. + markdown.clear(); + for (auto& block : codeBlocks) { + if (block.first == Text) { + // Replace all newlines with two spaces and a newline. + block.second.replace(newlineRegex, newline); + } + markdown += block.second; + } +} + +// A callback function that will be called by the md4c library (`md_html`) to output the HTML. +static void +htmlChunkCb(const MD_CHAR* data, MD_SIZE data_size, void* userData) +{ + QByteArray* array = static_cast<QByteArray*>(userData); + if (data_size > 0) { + array->append(data, int(data_size)); + } +}; + +QString +MessageParser::markdownToHtml(const char* markdown) +{ + static auto md_flags = MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_NOINDENTEDCODEBLOCKS + | MD_FLAG_TASKLISTS | MD_FLAG_STRIKETHROUGH | MD_FLAG_UNDERLINE; + size_t data_len = strlen(markdown); + if (data_len <= 0) { + return QString(); + } else { + QByteArray array; + int result = md_html(markdown, MD_SIZE(data_len), &htmlChunkCb, &array, md_flags, 0); + return result == 0 ? QString::fromUtf8(array) : QString(); + } +} diff --git a/src/app/messageparser.h b/src/app/messageparser.h new file mode 100644 index 0000000000000000000000000000000000000000..81edb5e9b066e53fdb2bba7776eb6d403ccc3cd2 --- /dev/null +++ b/src/app/messageparser.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QObject> +#include <QColor> +#include <QThreadPool> + +class PreviewEngine; +class HtmlParser; + +// This class is used to parse messages and encapsulate the logic that +// prepares a message for display in the UI. The basic steps are: +// 1. preprocess the markdown message (e.g. handle line breaks) +// 2. transform markdown syntax into HTML +// 3. generate previews for the first link in the message (if any) +// 4. add the appropriate CSS classes to the HTML (e.g. for links, code blocks, etc.) +// +// Step 3. is done asynchronously, so the message is displayed as soon as possible +// and the preview is added later. +class MessageParser final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(MessageParser) +public: + // Create a new MessageParser instance. We take an instance of PreviewEngine. + explicit MessageParser(PreviewEngine* previewEngine, QObject* parent = nullptr); + ~MessageParser() = default; + + // Parse the message. This will emit the messageParsed signal when the + // message is ready to be displayed. + void parseMessage(const QString& messageId, + const QString& msg, + bool previewLinks, + const QColor& linkColor, + const QColor& backgroundColor); + + // Emitted when the message is ready to be displayed. + Q_SIGNAL void messageParsed(const QString& msgId, const QString& msg); + + // Emitted when the message preview is ready to be displayed. + Q_SIGNAL void linkInfoReady(const QString& msgId, const QVariantMap& info); + +private: + // Preprocess the markdown message (e.g. handle line breaks). + void preprocessMarkdown(QString& markdown); + + // Transform markdown syntax into HTML. + QString markdownToHtml(const char* markdown); + + // Generate a preview for the given link, then emit the messageParsed signal. + void generatePreview(const QString& msgId, const QString& link); + + // The PreviewEngine instance used to generate previews. + PreviewEngine* previewEngine_; + + // An instance of HtmlParser used to parse HTML. + HtmlParser* htmlParser_; + + // Used to queue parse operations. + QThreadPool* threadPool_; +}; diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index dfbf0f7feedb5716f2e1a3bf3e90d322710ad226..0f18e2f7ed1d3af34a8fd0a5a2d2a40ea53f3031 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -25,6 +25,7 @@ #include "appsettingsmanager.h" #include "qtutils.h" +#include "messageparser.h" #include <api/datatransfermodel.h> @@ -48,7 +49,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, QObject* parent) : QmlAdapterBase(instance, parent) , settingsManager_(settingsManager) - , previewEngine_(previewEngine) + , messageParser_(new MessageParser(previewEngine, this)) , filteredMsgListModel_(new FilteredMsgListModel(this)) , mediaInteractions_(std::make_unique<MessageListModel>()) , timestampTimer_(new QTimer(this)) @@ -71,8 +72,8 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); }); - connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady); - connect(previewEngine_, &PreviewEngine::linkified, this, &MessagesAdapter::onMessageLinkified); + connect(messageParser_, &MessageParser::messageParsed, this, &MessagesAdapter::onMessageParsed); + connect(messageParser_, &MessageParser::linkInfoReady, this, &MessagesAdapter::onLinkInfoReady); connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated); timestampTimer_->start(timestampUpdateIntervalMs_); @@ -445,6 +446,24 @@ MessagesAdapter::onNewInteraction(const QString& convUid, } } +void +MessagesAdapter::onMessageParsed(const QString& messageId, const QString& parsed) +{ + const QString& convId = lrcInstance_->get_selectedConvUid(); + const QString& accId = lrcInstance_->get_currentAccountId(); + auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId); + conversation.interactions->setParsedMessage(messageId, parsed); +} + +void +MessagesAdapter::onLinkInfoReady(const QString& messageId, const QVariantMap& info) +{ + const QString& convId = lrcInstance_->get_selectedConvUid(); + const QString& accId = lrcInstance_->get_currentAccountId(); + auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId); + conversation.interactions->addHyperlinkInfo(messageId, info); +} + void MessagesAdapter::acceptInvitation(const QString& convId) { @@ -540,15 +559,6 @@ MessagesAdapter::removeContact(const QString& convUid, bool banContact) accInfo.contactModel->removeContact(contactUri, banContact); } -void -MessagesAdapter::onPreviewInfoReady(QString messageId, QVariantMap info) -{ - const QString& convId = lrcInstance_->get_selectedConvUid(); - const QString& accId = lrcInstance_->get_currentAccountId(); - auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId); - conversation.interactions->addHyperlinkInfo(messageId, info); -} - void MessagesAdapter::onConversationMessagesLoaded(uint32_t loadingRequestId, const QString& convId) { @@ -558,21 +568,13 @@ MessagesAdapter::onConversationMessagesLoaded(uint32_t loadingRequestId, const Q } void -MessagesAdapter::parseMessageUrls(const QString& messageId, - const QString& msg, - bool showPreview, - QColor color) +MessagesAdapter::parseMessage(const QString& msgId, + const QString& msg, + bool showPreview, + const QColor& linkColor, + const QColor& backgroundColor) { - previewEngine_->parseMessage(messageId, msg, showPreview, color); -} - -void -MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& linkified) -{ - const QString& convId = lrcInstance_->get_selectedConvUid(); - const QString& accId = lrcInstance_->get_currentAccountId(); - auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId); - conversation.interactions->linkifyMessage(messageId, linkified); + messageParser_->parseMessage(msgId, msg, showPreview, linkColor, backgroundColor); } void diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index 9d63b9e817ac565e7b512aa22c9ee1969654bdad..ec8bebfc8a0dfd25374897bf0258f129ed31ad2c 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -28,6 +28,9 @@ #include <QSortFilterProxyModel> +class AppSettingsManager; +class MessageParser; + class FilteredMsgListModel final : public QSortFilterProxyModel { Q_OBJECT @@ -50,8 +53,6 @@ public: }; }; -class AppSettingsManager; - class MessagesAdapter final : public QmlAdapterBase { Q_OBJECT @@ -74,7 +75,6 @@ Q_SIGNALS: void newMessageBarPlaceholderText(QString& placeholderText); void newFilePasted(QString filePath); void newTextPasted(); - void previewInformationToQML(QString messageId, QStringList previewInformation); void moreMessagesLoaded(qint32 loadingRequestId); void timestampUpdated(); void fileCopied(const QString& dest); @@ -123,10 +123,11 @@ protected: Q_INVOKABLE QString getFormattedDay(const quint64 timestamp); Q_INVOKABLE QString getFormattedTime(const quint64 timestamp); Q_INVOKABLE QString getBestFormattedDate(const quint64 timestamp); - Q_INVOKABLE void parseMessageUrls(const QString& messageId, - const QString& msg, - bool showPreview, - QColor color = "#0645AD"); + Q_INVOKABLE void parseMessage(const QString& msgId, + const QString& msg, + bool previewLinks, + const QColor& linkColor = QColor(0x06, 0x45, 0xad), + const QColor& backgroundColor = QColor(0x0, 0x0, 0x0)); Q_INVOKABLE void onPaste(); Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const; Q_INVOKABLE QString getStatusString(int status); @@ -147,9 +148,9 @@ private Q_SLOTS: void onNewInteraction(const QString& convUid, const QString& interactionId, const interaction::Info& interaction); - void onPreviewInfoReady(QString messageIndex, QVariantMap urlInMessage); + void onMessageParsed(const QString& messageId, const QString& parsed); + void onLinkInfoReady(const QString& messageIndex, const QVariantMap& info); void onConversationMessagesLoaded(uint32_t requestId, const QString& convId); - void onMessageLinkified(const QString& messageId, const QString& linkified); void onComposingStatusChanged(const QString& convId, const QString& contactUri, bool isComposing); @@ -160,7 +161,8 @@ private: QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet); AppSettingsManager* settingsManager_; - PreviewEngine* previewEngine_; + MessageParser* messageParser_; + FilteredMsgListModel* filteredMsgListModel_; static constexpr const int loadChunkSize_ {20}; diff --git a/src/app/networkmanager.cpp b/src/app/networkmanager.cpp index db45d4b92d8b5b1fb7a5446d28f81c4b57f8e125..4e84049942426e92fd49b73045ff4becc85e7346 100644 --- a/src/app/networkmanager.cpp +++ b/src/app/networkmanager.cpp @@ -1,7 +1,5 @@ -/*! +/* * Copyright (C) 2019-2023 Savoir-faire Linux Inc. - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,23 +18,29 @@ #include "networkmanager.h" #include "connectivitymonitor.h" -#include "utils.h" #include <QMetaEnum> #include <QtNetwork> -NetWorkManager::NetWorkManager(ConnectivityMonitor* cm, QObject* parent) +NetworkManager::NetworkManager(ConnectivityMonitor* cm, QObject* parent) : QObject(parent) , manager_(new QNetworkAccessManager(this)) - , reply_(nullptr) , connectivityMonitor_(cm) , lastConnectionState_(cm->isOnline()) { - Q_EMIT statusChanged(GetStatus::IDLE); - - connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, [this] { - cancelRequest(); - +#if QT_CONFIG(ssl) + connect(manager_, + &QNetworkAccessManager::sslErrors, + this, + [this](QNetworkReply* reply, const QList<QSslError>& errors) { + Q_UNUSED(reply); + Q_FOREACH (const QSslError& error, errors) { + qWarning() << Q_FUNC_INFO << error.errorString(); + Q_EMIT errorOccured(GetError::SSL_ERROR, error.errorString()); + } + }); +#endif + connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, this, [this] { auto connected = connectivityMonitor_->isOnline(); if (connected && !lastConnectionState_) { manager_->deleteLater(); @@ -48,126 +52,16 @@ NetWorkManager::NetWorkManager(ConnectivityMonitor* cm, QObject* parent) } void -NetWorkManager::get(const QUrl& url, const DoneCallBack& doneCb, const QString& path) +NetworkManager::sendGetRequest(const QUrl& url, + std::function<void(const QByteArray&)> onDoneCallback) { - if (!connectivityMonitor_->isOnline()) { - Q_EMIT errorOccured(GetError::DISCONNECTED); - return; - } - - if (reply_ && reply_->isRunning()) { - qWarning() << Q_FUNC_INFO << "currently downloading"; - return; - } else if (url.isEmpty()) { - qWarning() << Q_FUNC_INFO << "missing url"; - return; - } - - if (!path.isEmpty()) { - QFileInfo fileInfo(url.path()); - QString fileName = fileInfo.fileName(); - - file_.reset(new QFile(path + "/" + fileName)); - if (!file_->open(QIODevice::WriteOnly)) { - Q_EMIT errorOccured(GetError::ACCESS_DENIED); - file_.reset(nullptr); - return; + auto reply = manager_->get(QNetworkRequest(url)); + QObject::connect(reply, &QNetworkReply::finished, this, [reply, onDoneCallback]() { + if (reply->error() == QNetworkReply::NoError) { + onDoneCallback(reply->readAll()); + } else { + onDoneCallback(reply->errorString().toUtf8()); } - } - - QNetworkRequest request(url); - reply_ = manager_->get(request); - - Q_EMIT statusChanged(GetStatus::STARTED); - - connect(reply_, - QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred), - [this, doneCb, path](QNetworkReply::NetworkError error) { - reply_->disconnect(); - reset(true); - qWarning() << Q_FUNC_INFO << "NetworkError: " - << QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error); - Q_EMIT errorOccured(GetError::NETWORK_ERROR); - }); - - connect(reply_, &QNetworkReply::finished, [this, doneCb, path]() { - reply_->disconnect(); - QString response = {}; - if (path.isEmpty()) - response = QString(reply_->readAll()); - reset(!path.isEmpty()); - Q_EMIT statusChanged(GetStatus::FINISHED); - if (doneCb) - doneCb(response); + reply->deleteLater(); }); - - connect(reply_, - &QNetworkReply::downloadProgress, - this, - &NetWorkManager::downloadProgressChanged); - - connect(reply_, &QNetworkReply::readyRead, this, &NetWorkManager::onHttpReadyRead); - -#if QT_CONFIG(ssl) - connect(reply_, - SIGNAL(sslErrors(const QList<QSslError>&)), - this, - SLOT(onSslErrors(QList<QSslError>)), - Qt::UniqueConnection); -#endif -} - -void -NetWorkManager::reset(bool flush) -{ - reply_->deleteLater(); - reply_ = nullptr; - if (file_ && flush) { - file_->flush(); - file_->close(); - file_.reset(nullptr); - } -} - -void -NetWorkManager::onSslErrors(const QList<QSslError>& sslErrors) -{ -#if QT_CONFIG(ssl) - reply_->disconnect(); - reset(true); - - QString errorsString; - for (const QSslError& error : sslErrors) { - if (errorsString.length() > 0) { - errorsString += "\n"; - } - errorsString += error.errorString(); - } - Q_EMIT errorOccured(GetError::SSL_ERROR, errorsString); - return; -#else - Q_UNUSED(sslErrors); -#endif -} - -void -NetWorkManager::onHttpReadyRead() -{ - /* - * This slot gets called every time the QNetworkReply has new data. - * We read all of its new data and write it into the file. - * That way we use less RAM than when reading it at the finished() - * signal of the QNetworkReply - */ - if (file_) - file_->write(reply_->readAll()); -} - -void -NetWorkManager::cancelRequest() -{ - if (reply_) { - reply_->abort(); - Q_EMIT errorOccured(GetError::CANCELED); - } } diff --git a/src/app/networkmanager.h b/src/app/networkmanager.h index 2609834b96b7294bc06f705db6f0ec7685c3e89d..bf80b0d95c0c2cdb7ac374063292218eee6dd397 100644 --- a/src/app/networkmanager.h +++ b/src/app/networkmanager.h @@ -1,7 +1,5 @@ -/*! +/* * Copyright (C) 2019-2023 Savoir-faire Linux Inc. - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,50 +25,26 @@ class QNetworkAccessManager; class ConnectivityMonitor; -class NetWorkManager : public QObject +class NetworkManager : public QObject { Q_OBJECT public: - explicit NetWorkManager(ConnectivityMonitor* cm, QObject* parent = nullptr); - virtual ~NetWorkManager() = default; - - enum GetStatus { IDLE, STARTED, FINISHED }; + explicit NetworkManager(ConnectivityMonitor* cm, QObject* parent = nullptr); + virtual ~NetworkManager() = default; enum GetError { DISCONNECTED, NETWORK_ERROR, ACCESS_DENIED, SSL_ERROR, CANCELED }; Q_ENUM(GetError) - using DoneCallBack = std::function<void(const QString&)>; - - /*! - * using qt get request to store the reply in file - * @param url - network address - * @param doneCb - done callback - * @param path - optional file saving path, if empty - * a string will be passed as the second paramter of doneCb - */ - void get(const QUrl& url, const DoneCallBack& doneCb = {}, const QString& path = {}); - - /*! - * manually abort the current request - */ - Q_INVOKABLE void cancelRequest(); + void sendGetRequest(const QUrl& url, std::function<void(const QByteArray&)> onDoneCallback); Q_SIGNALS: - void statusChanged(GetStatus error); - void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes); void errorOccured(GetError error, const QString& msg = {}); -private Q_SLOTS: - void onSslErrors(const QList<QSslError>& sslErrors); - void onHttpReadyRead(); +protected: + QNetworkAccessManager* manager_; private: - void reset(bool flush = true); - - QNetworkAccessManager* manager_; - QNetworkReply* reply_; - QScopedPointer<QFile> file_; ConnectivityMonitor* connectivityMonitor_; bool lastConnectionState_; }; -Q_DECLARE_METATYPE(NetWorkManager*) +Q_DECLARE_METATYPE(NetworkManager*) diff --git a/src/app/nowebengine/previewengine.cpp b/src/app/nowebengine/previewengine.cpp deleted file mode 100644 index aeab27467fad491aea86b567e9667dfa7a5b8b64..0000000000000000000000000000000000000000 --- a/src/app/nowebengine/previewengine.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2022-2023 Savoir-faire Linux Inc. - * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -#include "previewengine.h" - -struct PreviewEngine::Impl : public QObject -{ - Impl(PreviewEngine&) - : QObject(nullptr) - {} -}; - -PreviewEngine::PreviewEngine(QObject* parent) - : QObject(parent) - , pimpl_(std::make_unique<Impl>(*this)) -{} - -PreviewEngine::~PreviewEngine() {} - -void -PreviewEngine::parseMessage(const QString&, const QString&, bool, QColor) -{} - -void -PreviewEngine::log(const QString&) -{} - -void -PreviewEngine::emitInfoReady(const QString&, const QVariantMap&) -{} - -void -PreviewEngine::emitLinkified(const QString&, const QString&) -{} - -#include "moc_previewengine.cpp" -#include "previewengine.moc" diff --git a/src/app/os/macos/updatemanager.mm b/src/app/os/macos/updatemanager.mm index ac4d73dcf502c5bb3cbdc52b2f93c0dc798907d4..124f081cd7d1b68d80657204edd799d65cfcff46 100644 --- a/src/app/os/macos/updatemanager.mm +++ b/src/app/os/macos/updatemanager.mm @@ -84,7 +84,7 @@ UpdateManager::UpdateManager(const QString& url, ConnectivityMonitor* cm, LRCInstance* instance, QObject* parent) - : NetWorkManager(cm, parent) + : NetworkManager(cm, parent) , pimpl_(std::make_unique<Impl>()) {} diff --git a/src/app/previewengine.cpp b/src/app/previewengine.cpp index 2b8609be3553362e272e140b52d1d13c7ffee7c9..bd9f1bc65500cc3fd1b558dd0cc94ee8bb089ec5 100644 --- a/src/app/previewengine.cpp +++ b/src/app/previewengine.cpp @@ -1,7 +1,5 @@ /* * Copyright (C) 2021-2023 Savoir-faire Linux Inc. - * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com> - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,101 +17,103 @@ #include "previewengine.h" -#include "utils.h" +#include <QRegularExpression> -#include <QWebEngineScript> -#include <QWebEngineProfile> -#include <QWebEngineSettings> - -#include <QtWebChannel> -#include <QWebEnginePage> - -struct PreviewEngine::Impl : public QWebEnginePage +static QString +getInnerHtml(const QString& tag) { -public: - PreviewEngine& parent_; - QWebChannel* channel_; - - Impl(PreviewEngine& parent) - : QWebEnginePage((QObject*) nullptr) - , parent_(parent) - { - QWebEngineProfile* profile = QWebEngineProfile::defaultProfile(); - - QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); - dataDir.cdUp(); - auto cachePath = dataDir.absolutePath() + "/jami"; - profile->setCachePath(cachePath); - profile->setPersistentStoragePath(cachePath); - profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); - profile->setHttpCacheType(QWebEngineProfile::NoCache); - - settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); - settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false); - settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); - settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); - settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false); - settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false); - settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); - settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true); - settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true); - settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false); - settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true); + static const QRegularExpression re(">([^<]+)<"); + const auto match = re.match(tag); + return match.hasMatch() ? match.captured(1) : QString {}; +}; - channel_ = new QWebChannel(this); - channel_->registerObject(QStringLiteral("jsbridge"), &parent_); +const QRegularExpression PreviewEngine::newlineRe("\\n"); - setWebChannel(channel_); - runJavaScript(Utils::QByteArrayFromFile(":webengine/linkify.js"), - QWebEngineScript::MainWorld); - runJavaScript(Utils::QByteArrayFromFile(":webengine/linkify-string.js"), - QWebEngineScript::MainWorld); - runJavaScript(Utils::QByteArrayFromFile(":webengine/qwebchannel.js"), - QWebEngineScript::MainWorld); - runJavaScript(Utils::QByteArrayFromFile(":webengine/previewInfo.js"), - QWebEngineScript::MainWorld); - runJavaScript(Utils::QByteArrayFromFile(":webengine/previewInterop.js"), - QWebEngineScript::MainWorld); - } +PreviewEngine::PreviewEngine(ConnectivityMonitor* cm, QObject* parent) + : NetworkManager(cm, parent) + , htmlParser_(new HtmlParser(this)) +{ + // Connect on a queued connection to avoid blocking caller thread. + connect(this, &PreviewEngine::parseLink, this, &PreviewEngine::onParseLink, Qt::QueuedConnection); +} - void parseMessage(const QString& messageId, const QString& msg, bool showPreview, QColor color) - { - QString colorStr = "'" + color.name() + "'"; - runJavaScript(QString("parseMessage(`%1`, `%2`, %3, %4)") - .arg(messageId, msg, showPreview ? "true" : "false", colorStr)); +QString +PreviewEngine::getTagContent(QList<QString>& tags, const QString& value) +{ + Q_FOREACH (auto tag, tags) { + const QRegularExpression re("(property|name)=\"(og:|twitter:|)" + value + + "\".*?content=\"([^\"]+)\""); + + const auto match = re.match(tag.remove(newlineRe)); + if (match.hasMatch()) { + return match.captured(3); + } } -}; - -PreviewEngine::PreviewEngine(QObject* parent) - : QObject(parent) - , pimpl_(std::make_unique<Impl>(*this)) -{} - -PreviewEngine::~PreviewEngine() {} + return QString {}; +} -void -PreviewEngine::parseMessage(const QString& messageId, - const QString& msg, - bool showPreview, - QColor color) +QString +PreviewEngine::getTitle(HtmlParser::TagInfoList& metaTags) { - pimpl_->parseMessage(messageId, msg, showPreview, color); + // Try with opengraph/twitter props + QString title = getTagContent(metaTags[TidyTag_META], "title"); + if (title.isEmpty()) { // Try with title tag + title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_TITLE)); + } + if (title.isEmpty()) { // Try with h1 tag + title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_H1)); + } + if (title.isEmpty()) { // Try with h2 tag + title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_H2)); + } + return title; } -void -PreviewEngine::log(const QString& str) +QString +PreviewEngine::getDescription(HtmlParser::TagInfoList& metaTags) { - qDebug() << str; + // Try with og/twitter props + QString d = getTagContent(metaTags[TidyTag_META], "description"); + if (d.isEmpty()) { // Try with first paragraph + d = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_P)); + } + return d; } -void -PreviewEngine::emitInfoReady(const QString& messageId, const QVariantMap& info) +QString +PreviewEngine::getImage(HtmlParser::TagInfoList& metaTags) { - Q_EMIT infoReady(messageId, info); + static const QRegularExpression newlineRe("\\n"); + // Try with og/twitter props + QString image = getTagContent(metaTags[TidyTag_META], "image"); + if (image.isEmpty()) { // Try with href of link tag (rel="image_src") + auto tags = htmlParser_->getTags({TidyTag_LINK}); + Q_FOREACH (auto tag, tags[TidyTag_LINK]) { + static const QRegularExpression re("rel=\"image_src\".*?href=\"([^\"]+)\""); + const auto match = re.match(tag.remove(newlineRe)); + if (match.hasMatch()) { + return match.captured(1); + } + } + } + return image; } void -PreviewEngine::emitLinkified(const QString& messageId, const QString& linkifiedStr) +PreviewEngine::onParseLink(const QString& messageId, const QString& link) { - Q_EMIT linkified(messageId, linkifiedStr); + sendGetRequest(QUrl(link), [this, messageId, link](const QByteArray& html) { + htmlParser_->parseHtmlString(html); + auto metaTags = htmlParser_->getTags({TidyTag_META}); + QString domain = QUrl(link).host(); + if (domain.isEmpty()) { + domain = link; + } + Q_EMIT infoReady(messageId, + {{"title", getTitle(metaTags)}, + {"description", getDescription(metaTags)}, + {"image", getImage(metaTags)}, + {"url", link}, + {"domain", domain}}); + }); } diff --git a/src/app/previewengine.h b/src/app/previewengine.h index be9e3f6b5c44ac6e239362ea66a40fdb4bf6d611..db14a96886179febfea78db377d11ed95e996a03 100644 --- a/src/app/previewengine.h +++ b/src/app/previewengine.h @@ -1,7 +1,5 @@ /* * Copyright (C) 2021-2023 Savoir-faire Linux Inc. - * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com> - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,31 +17,32 @@ #pragma once -#include <QColor> -#include <QObject> +#include "networkmanager.h" -class PreviewEngine : public QObject +#include "htmlparser.h" + +class PreviewEngine final : public NetworkManager { Q_OBJECT Q_DISABLE_COPY(PreviewEngine) public: - PreviewEngine(QObject* parent = nullptr); - ~PreviewEngine(); - - void parseMessage(const QString& messageId, - const QString& msg, - bool showPreview, - QColor color = "#0645AD"); - - Q_INVOKABLE void log(const QString& str); - Q_INVOKABLE void emitInfoReady(const QString& messageId, const QVariantMap& info); - Q_INVOKABLE void emitLinkified(const QString& messageId, const QString& linkifiedStr); + PreviewEngine(ConnectivityMonitor* cm, QObject* parent = nullptr); + ~PreviewEngine() = default; Q_SIGNALS: + void parseLink(const QString& messageId, const QString& link); void infoReady(const QString& messageId, const QVariantMap& info); - void linkified(const QString& messageId, const QString& linkifiedStr); private: - struct Impl; - std::unique_ptr<Impl> pimpl_; + Q_SLOT void onParseLink(const QString& messageId, const QString& link); + + // An instance of HtmlParser used to parse HTML. + HtmlParser* htmlParser_; + + QString getTagContent(QList<QString>& tags, const QString& value); + QString getTitle(HtmlParser::TagInfoList& metaTags); + QString getDescription(HtmlParser::TagInfoList& metaTags); + QString getImage(HtmlParser::TagInfoList& metaTags); + + static const QRegularExpression newlineRe; }; diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index bc7a88dd49d016d43ebd8b9bf6ad8b855447766f..f3f9ffe97637db388b34100126730534ab180d93 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -226,7 +226,7 @@ registerTypes(QQmlEngine* engine, // Enums QML_REGISTERUNCREATABLE(NS_ENUMS, Settings) - QML_REGISTERUNCREATABLE(NS_ENUMS, NetWorkManager) + QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager) QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel) QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel) diff --git a/src/app/settingsview/components/UpdateDownloadDialog.qml b/src/app/settingsview/components/UpdateDownloadDialog.qml index 74d0894e18b09d030db4121fa7321ebaac553625..e0bab64a715af78cb88d99f5640375dad0090e3e 100644 --- a/src/app/settingsview/components/UpdateDownloadDialog.qml +++ b/src/app/settingsview/components/UpdateDownloadDialog.qml @@ -36,7 +36,7 @@ SimpleMessageDialog { Connections { target: UpdateManager - function onUpdateDownloadProgressChanged(bytesRead, totalBytes) { + function onDownloadProgressChanged(bytesRead, totalBytes) { downloadDialog.setDownloadProgress(bytesRead, totalBytes); } @@ -98,10 +98,10 @@ SimpleMessageDialog { buttonTitles: [JamiStrings.optionCancel] buttonStyles: [SimpleMessageDialog.ButtonStyle.TintedBlue] buttonCallBacks: [function () { - UpdateManager.cancelUpdate(); + UpdateManager.cancelDownload(); }] onVisibleChanged: { if (!visible) - UpdateManager.cancelUpdate(); + UpdateManager.cancelDownload(); } } diff --git a/src/app/updatemanager.cpp b/src/app/updatemanager.cpp index 652fbaca127da2fbfa805eb61c6d1cdbbdcca80f..180cd2b3f624aac1a065a86f1e4cd293e30e00fb 100644 --- a/src/app/updatemanager.cpp +++ b/src/app/updatemanager.cpp @@ -39,7 +39,7 @@ static constexpr char betaMsiSubUrl[] = "/beta/jami.beta.x64.msi"; struct UpdateManager::Impl : public QObject { - Impl(const QString& url, ConnectivityMonitor* cm, LRCInstance* instance, UpdateManager& parent) + Impl(const QString& url, LRCInstance* instance, UpdateManager& parent) : QObject(nullptr) , parent_(parent) , lrcInstance_(instance) @@ -60,14 +60,14 @@ struct UpdateManager::Impl : public QObject // Fail without UI if this is a programmatic check. if (!quiet) connect(&parent_, - &NetWorkManager::errorOccured, + &NetworkManager::errorOccured, &parent_, &UpdateManager::updateErrorOccurred); cleanUpdateFiles(); QUrl versionUrl {isBeta ? QUrl::fromUserInput(baseUrlString_ + betaVersionSubUrl) : QUrl::fromUserInput(baseUrlString_ + versionSubUrl)}; - parent_.get(versionUrl, [this, quiet](const QString& latestVersionString) { + parent_.sendGetRequest(versionUrl, [this, quiet](const QByteArray& latestVersionString) { if (latestVersionString.isEmpty()) { qWarning() << "Error checking version"; if (!quiet) @@ -92,16 +92,12 @@ struct UpdateManager::Impl : public QObject { parent_.disconnect(); connect(&parent_, - &NetWorkManager::errorOccured, + &NetworkManager::errorOccured, &parent_, &UpdateManager::updateErrorOccurred); - connect(&parent_, &NetWorkManager::statusChanged, this, [this](GetStatus status) { + connect(&parent_, &UpdateManager::statusChanged, this, [this](GetStatus status) { switch (status) { case GetStatus::STARTED: - connect(&parent_, - &NetWorkManager::downloadProgressChanged, - &parent_, - &UpdateManager::updateDownloadProgressChanged); Q_EMIT parent_.updateDownloadStarted(); break; case GetStatus::FINISHED: @@ -115,9 +111,11 @@ struct UpdateManager::Impl : public QObject QUrl downloadUrl {(beta || isBeta) ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl) : QUrl::fromUserInput(baseUrlString_ + msiSubUrl)}; - parent_.get( + parent_.downloadFile( downloadUrl, - [this, downloadUrl](const QString&) { + [this, downloadUrl](bool success, const QString& errorMessage) { + Q_UNUSED(success) + Q_UNUSED(errorMessage) lrcInstance_->finish(); Q_EMIT lrcInstance_->quitEngineRequested(); auto args = QString(" /passive /norestart WIXNONUILAUNCH=1"); @@ -132,7 +130,7 @@ struct UpdateManager::Impl : public QObject void cancelUpdate() { - parent_.cancelRequest(); + parent_.cancelDownload(); }; void setAutoUpdateCheck(bool state) @@ -175,11 +173,14 @@ UpdateManager::UpdateManager(const QString& url, ConnectivityMonitor* cm, LRCInstance* instance, QObject* parent) - : NetWorkManager(cm, parent) - , pimpl_(std::make_unique<Impl>(url, cm, instance, *this)) + : NetworkManager(cm, parent) + , pimpl_(std::make_unique<Impl>(url, instance, *this)) {} -UpdateManager::~UpdateManager() {} +UpdateManager::~UpdateManager() +{ + cancelDownload(); +} void UpdateManager::checkForUpdates(bool quiet) @@ -225,3 +226,112 @@ UpdateManager::isAutoUpdaterEnabled() { return false; } + +void +UpdateManager::cancelDownload() +{ + if (downloadReply_) { + Q_EMIT errorOccured(GetError::CANCELED); + downloadReply_->abort(); + resetDownload(); + } +} + +void +UpdateManager::downloadFile(const QUrl& url, + std::function<void(bool, const QString&)> onDoneCallback, + const QString& filePath) +{ + // If there is already a download in progress, return. + if (downloadReply_ && downloadReply_->isRunning()) { + qWarning() << Q_FUNC_INFO << "Download already in progress"; + return; + } + + // Clean up any previous download. + resetDownload(); + + // If the url is invalid, return. + if (!url.isValid()) { + Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid url"); + return; + } + + // If the file path is empty, return. + if (filePath.isEmpty()) { + Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid file path"); + return; + } + + // Create the file. Return if it cannot be created. + QFileInfo fileInfo(url.path()); + QString fileName = fileInfo.fileName(); + file_.reset(new QFile(filePath + "/" + fileName)); + if (!file_->open(QIODevice::WriteOnly)) { + Q_EMIT errorOccured(GetError::ACCESS_DENIED); + file_.reset(); + qWarning() << Q_FUNC_INFO << "Could not open file for writing"; + return; + } + + // Start the download. + QNetworkRequest request(url); + downloadReply_ = manager_->get(request); + + connect(downloadReply_, &QNetworkReply::readyRead, this, [=]() { + if (file_ && file_->isOpen()) { + file_->write(downloadReply_->readAll()); + } + }); + + connect(downloadReply_, + &QNetworkReply::downloadProgress, + this, + [=](qint64 bytesReceived, qint64 bytesTotal) { + Q_EMIT downloadProgressChanged(bytesReceived, bytesTotal); + }); + + connect(downloadReply_, + QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred), + this, + [this](QNetworkReply::NetworkError error) { + downloadReply_->disconnect(); + resetDownload(); + qWarning() << Q_FUNC_INFO + << QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error); + Q_EMIT errorOccured(GetError::NETWORK_ERROR); + }); + + connect(downloadReply_, &QNetworkReply::finished, this, [this, onDoneCallback]() { + bool success = false; + QString errorMessage; + if (downloadReply_->error() == QNetworkReply::NoError) { + resetDownload(); + success = true; + } else { + errorMessage = downloadReply_->errorString(); + resetDownload(); + } + onDoneCallback(success, errorMessage); + Q_EMIT statusChanged(GetStatus::FINISHED); + }); + + Q_EMIT statusChanged(GetStatus::STARTED); +} + +void +UpdateManager::resetDownload() +{ + if (downloadReply_) { + downloadReply_->deleteLater(); + downloadReply_ = nullptr; + } + if (file_) { + if (file_->isOpen()) { + file_->flush(); + file_->close(); + } + file_->deleteLater(); + file_.reset(); + } +} diff --git a/src/app/updatemanager.h b/src/app/updatemanager.h index ab4f19d12a4b3baf6e6268e410b43e08ca258498..c28fe75a918f79eccf22365c6743f05f8985794d 100644 --- a/src/app/updatemanager.h +++ b/src/app/updatemanager.h @@ -25,7 +25,7 @@ class LRCInstance; class ConnectivityMonitor; -class UpdateManager final : public NetWorkManager +class UpdateManager final : public NetworkManager { Q_OBJECT Q_DISABLE_COPY(UpdateManager) @@ -36,6 +36,9 @@ public: QObject* parent = nullptr); ~UpdateManager(); + enum GetStatus { STARTED, FINISHED }; + Q_ENUM(GetStatus) + Q_INVOKABLE void checkForUpdates(bool quiet = false); Q_INVOKABLE void applyUpdates(bool beta = false); Q_INVOKABLE void cancelUpdate(); @@ -43,15 +46,28 @@ public: Q_INVOKABLE bool isCurrentVersionBeta(); Q_INVOKABLE bool isUpdaterEnabled(); Q_INVOKABLE bool isAutoUpdaterEnabled(); + Q_INVOKABLE void cancelDownload(); + + void downloadFile(const QUrl& url, + std::function<void(bool, const QString&)> onDoneCallback, + const QString& filePath); Q_SIGNALS: + void statusChanged(GetStatus status); + void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes); + void updateCheckReplyReceived(bool ok, bool found = false); - void updateErrorOccurred(const NetWorkManager::GetError& error); + void updateErrorOccurred(const NetworkManager::GetError& error); void updateDownloadStarted(); void updateDownloadProgressChanged(qint64 bytesRead, qint64 totalBytes); void updateDownloadFinished(); void appCloseRequested(); +private: + void resetDownload(); + QNetworkReply* downloadReply_ {nullptr}; + QScopedPointer<QFile> file_; + private: struct Impl; std::unique_ptr<Impl> pimpl_; diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h index 26b5a3861aa6ae252e6ee8f3880e21a965752685..1c07b320c56fc5db023c3074f72190b9aa058140 100644 --- a/src/libclient/api/interaction.h +++ b/src/libclient/api/interaction.h @@ -297,7 +297,7 @@ public: * @var isRead * @var commit * @var linkPreviewInfo - * @var linkified + * @var parsedBody */ struct Info { @@ -312,7 +312,7 @@ struct Info bool isRead = false; MapStringString commit; QVariantMap linkPreviewInfo = {}; - QString linkified; + QString parsedBody = {}; QVariantMap reactions; QString react_to; QVector<Body> previousBodies; diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index 1ab2eb5dc5dbd75aff48dc9c83b709af614d33d1..108165ca986b6da7210f501b85eb1bbc0dfd97ae 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -454,14 +454,8 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(item.second.isRead); case Role::LinkPreviewInfo: return QVariant(item.second.linkPreviewInfo); - case Role::Linkified: - return QVariant(item.second.linkified); - case Role::LinkifiedBody: { - if (!item.second.linkified.isEmpty()) { - return QVariant(item.second.linkified); - } - return QVariant(item.second.body); - } + case Role::ParsedBody: + return QVariant(item.second.parsedBody); case Role::ActionUri: return QVariant(item.second.commit["uri"]); case Role::ConfId: @@ -484,9 +478,9 @@ MessageListModel::dataForItem(item_t item, int, int role) const case Role::ReplyToBody: { if (repliedMsg == -1) return QVariant(""); - auto linkified = data(repliedMsg, Role::Linkified).toString(); - if (!linkified.isEmpty()) - return QVariant(linkified); + auto parsed = data(repliedMsg, Role::ParsedBody).toString(); + if (!parsed.isEmpty()) + return QVariant(parsed); return QVariant(data(repliedMsg, Role::Body).toString()); } case Role::TotalSize: @@ -551,15 +545,15 @@ MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& } void -MessageListModel::linkifyMessage(const QString& messageId, const QString& linkified) +MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed) { int index = getIndexOfMessage(messageId); if (index == -1) { return; } QModelIndex modelIndex = QAbstractListModel::index(index, 0); - interactions_[index].second.linkified = linkified; - Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Linkified, Role::LinkifiedBody}); + interactions_[index].second.parsedBody = parsed; + Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody}); } void @@ -712,12 +706,11 @@ MessageListModel::editMessage(const QString& msgId, interaction::Info& info) } } info.body = it->rbegin()->body; - info.linkified.clear(); + info.parsedBody.clear(); editedBodies_.erase(it); emitDataChanged(msgId, {MessageList::Role::Body, - MessageList::Role::Linkified, - MessageList::Role::LinkifiedBody, + MessageList::Role::ParsedBody, MessageList::Role::PreviousBodies, MessageList::Role::IsEmojiOnly}); diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index 5a6c14def80bd4a5d00c32b1358e2dad1e8111ff..7345862fdf62f09feea7b5e4fc2e69c425797c97 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -45,8 +45,7 @@ struct Info; X(ConfId) \ X(DeviceId) \ X(LinkPreviewInfo) \ - X(Linkified) \ - X(LinkifiedBody) \ + X(ParsedBody) \ X(PreviousBodies) \ X(Reactions) \ X(ReplyTo) \ @@ -124,7 +123,7 @@ public: bool contains(const QString& msgId); int getIndexOfMessage(const QString& messageId) const; void addHyperlinkInfo(const QString& messageId, const QVariantMap& info); - void linkifyMessage(const QString& messageId, const QString& linkified); + void setParsedMessage(const QString& messageId, const QString& parsed); void setRead(const QString& peer, const QString& messageId); QString getRead(const QString& peer); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a0797e41295e9f818c5caab5352af1d44ffa3183..d50908cfafa38450f72e40d1c049793e23b7d935 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,12 +38,18 @@ set(TEST_QML_RESOURCES ${CMAKE_SOURCE_DIR}/src/app/resources.qrc) # Common jami files -add_library(test_common_obj OBJECT ${COMMON_SOURCES} ${COMMON_HEADERS}) +add_library(test_common_obj OBJECT + ${COMMON_SOURCES} + ${COMMON_HEADERS}) + +target_include_directories(test_common_obj PRIVATE + ${CLIENT_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/src) +target_link_directories(test_common_obj PRIVATE ${CLIENT_LINK_DIRS}) target_link_libraries(test_common_obj ${QML_TEST_LIBS}) target_compile_definitions(test_common_obj PRIVATE ENABLE_TESTS="ON") -include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src) - set(COMMON_TESTS_SOURCES ${QML_RESOURCES} ${QML_RESOURCES_QML} @@ -77,28 +83,18 @@ set(UNIT_TESTS_SOURCE_FILES ${CMAKE_SOURCE_DIR}/tests/unittests/main_unittest.cpp ${CMAKE_SOURCE_DIR}/tests/unittests/account_unittest.cpp ${CMAKE_SOURCE_DIR}/tests/unittests/contact_unittest.cpp + ${CMAKE_SOURCE_DIR}/tests/unittests/messageparser_unittest.cpp ${CMAKE_SOURCE_DIR}/tests/unittests/globaltestenvironment.h ${COMMON_TESTS_SOURCES}) set(ALL_TESTS_LIBS ${QML_TEST_LIBS} gtest - ${ringclient} - ${qrencode} - ${X11} - ${LIBNM_LIBRARIES} - ${LIBNOTIFY_LIBRARIES} - ${LIBGDKPIXBUF_LIBRARIES} - ${WINDOWS_LIBS}) + ${CLIENT_LIBS}) set(ALL_TESTS_INCLUDES ${TESTS_INCLUDES} - ${LRC}/include/libringclient - ${LRC}/include - ${LIBNM_INCLUDE_DIRS} - ${LIBNOTIFY_INCLUDE_DIRS} - ${LIBGDKPIXBUF_INCLUDE_DIRS} - ${WINDOWS_INCLUDES}) + ${CLIENT_INCLUDE_DIRS}) function(setup_test TEST_NAME TEST_SOURCES TEST_INPUT) string(TOLOWER ${TEST_NAME} TEST_BINARY_NAME) @@ -120,4 +116,4 @@ setup_test(Qml_Tests # Unit tests setup_test(Unit_Tests - "${UNIT_TESTS_SOURCE_FILES}" "") \ No newline at end of file + "${UNIT_TESTS_SOURCE_FILES}" "") diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp index d7c0165f518e901b495e90a20ce30a4841f7a405..4ab0725d47b51acabbf5a7dba085928e2896a31d 100644 --- a/tests/qml/main.cpp +++ b/tests/qml/main.cpp @@ -59,7 +59,7 @@ public Q_SLOTS: connectivityMonitor_.reset(new ConnectivityMonitor(this)); settingsManager_.reset(new AppSettingsManager(this)); systemTray_.reset(new SystemTray(settingsManager_.get(), this)); - previewEngine_.reset(new PreviewEngine(this)); + previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this)); QFontDatabase::addApplicationFont(":/images/FontAwesome.otf"); diff --git a/tests/unittests/globaltestenvironment.h b/tests/unittests/globaltestenvironment.h index ae69eb8557ef0d97817a2c26b01985f08ca8673b..8a45c229e41da5b23688df3abab0bd8d9ad47f0e 100644 --- a/tests/unittests/globaltestenvironment.h +++ b/tests/unittests/globaltestenvironment.h @@ -21,7 +21,8 @@ #include "appsettingsmanager.h" #include "connectivitymonitor.h" #include "systemtray.h" - +#include "previewengine.h" +#include "messageparser.h" #include "accountadapter.h" #include <QTest> @@ -55,6 +56,9 @@ public: systemTray.get(), lrcInstance.data(), nullptr)); + + previewEngine.reset(new PreviewEngine(connectivityMonitor.get(), nullptr)); + messageParser.reset(new MessageParser(previewEngine.data(), nullptr)); } void TearDown() @@ -75,6 +79,8 @@ public: QScopedPointer<ConnectivityMonitor> connectivityMonitor; QScopedPointer<AppSettingsManager> settingsManager; QScopedPointer<SystemTray> systemTray; + QScopedPointer<PreviewEngine> previewEngine; + QScopedPointer<MessageParser> messageParser; }; extern TestEnvironment globalEnv; diff --git a/tests/unittests/messageparser_unittest.cpp b/tests/unittests/messageparser_unittest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4cbe16d9509c878ba88ec6573dd25709a5c39748 --- /dev/null +++ b/tests/unittests/messageparser_unittest.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "globaltestenvironment.h" + +class MessageParserFixture : public ::testing::Test +{ +public: + // Prepare unit test context. Called at + // prior each unit test execution + void SetUp() override {} + + // Close unit test context. Called + // after each unit test ending + void TearDown() override {} +}; + +/*! + * WHEN We parse a markdown text body with no link. + * THEN The HTML body should be returned correctly without the link. + */ +TEST_F(MessageParserFixture, TextIsParsedCorrectly) +{ + auto linkColor = QColor::fromRgb(0, 0, 255); + auto backgroundColor = QColor::fromRgb(0, 0, 255); + + QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed); + QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady); + + globalEnv.messageParser->parseMessage("msgId_01", + "This is a **bold** text", + true, + linkColor, + backgroundColor); + + // Wait for the messageParsed signal which should be emitted once. + messageParsedSpy.wait(); + EXPECT_EQ(messageParsedSpy.count(), 1); + + QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst(); + EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_01"); + EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(1).toString(), + "<style></style><p>This is a <strong>bold</strong> text</p>\n"); + + // No link info should be returned. + linkInfoReadySpy.wait(); + EXPECT_EQ(linkInfoReadySpy.count(), 0); +} + +/*! + * WHEN We parse a text body with a link. + * THEN The HTML body should be returned correctly including the link. + */ +TEST_F(MessageParserFixture, ALinkIsParsedCorrectly) +{ + auto linkColor = QColor::fromRgb(0, 0, 255); + auto backgroundColor = QColor::fromRgb(0, 0, 255); + + QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed); + QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady); + + // Parse a message with a link. + globalEnv.messageParser->parseMessage("msgId_02", + "https://www.google.com", + true, + linkColor, + backgroundColor); + + // Wait for the messageParsed signal which should be emitted once. + messageParsedSpy.wait(); + EXPECT_EQ(messageParsedSpy.count(), 1); + + QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst(); + EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_02"); + EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(1).toString(), + "<style>a{color:#0000ff;}</style><p><a " + "href=\"https://www.google.com\">https://www.google.com</a></p>\n"); + + // Wait for the linkInfoReady signal which should be emitted once. + linkInfoReadySpy.wait(); + EXPECT_EQ(linkInfoReadySpy.count(), 1); + + QList<QVariant> linkInfoReadyArguments = linkInfoReadySpy.takeFirst(); + EXPECT_TRUE(linkInfoReadyArguments.at(0).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(linkInfoReadyArguments.at(0).toString(), "msgId_02"); + EXPECT_TRUE(linkInfoReadyArguments.at(1).typeId() == qMetaTypeId<QVariantMap>()); + QVariantMap linkInfo = linkInfoReadyArguments.at(1).toMap(); + EXPECT_EQ(linkInfo["url"].toString(), "https://www.google.com"); + // The rest of the link info is not tested here. +} + +/*! + * WHEN We parse a text body with end of line characters. + * THEN The HTML body should be returned correctly with the end of line characters. + */ +TEST_F(MessageParserFixture, EndOfLineCharactersAreParsedCorrectly) +{ + auto linkColor = QColor::fromRgb(0, 0, 255); + auto backgroundColor = QColor::fromRgb(0, 0, 255); + + QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed); + QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady); + + // Parse a message with a link. + globalEnv.messageParser->parseMessage("msgId_03", + "Text with\n2 lines", + true, + linkColor, + backgroundColor); + + // Wait for the messageParsed signal which should be emitted once. + messageParsedSpy.wait(); + EXPECT_EQ(messageParsedSpy.count(), 1); + + QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst(); + EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_03"); + EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(1).toString(), + "<style></style><p>Text with<br>\n2 lines</p>\n"); +} + +/*! + * WHEN We parse a text body with some fenced code. + * THEN The HTML body should be returned correctly with the code wrapped in a <pre> tag. + */ +TEST_F(MessageParserFixture, FencedCodeIsParsedCorrectly) +{ + auto linkColor = QColor::fromRgb(0, 0, 255); + auto backgroundColor = QColor::fromRgb(0, 0, 255); + + QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed); + QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady); + + // Parse a message with a link. + globalEnv.messageParser->parseMessage("msgId_04", + "Text with \n```\ncode\n```", + true, + linkColor, + backgroundColor); + + // Wait for the messageParsed signal which should be emitted once. + messageParsedSpy.wait(); + EXPECT_EQ(messageParsedSpy.count(), 1); + + QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst(); + EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_04"); + EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>()); + EXPECT_EQ(messageParserArguments.at(1).toString(), + "<style>pre,code{background-color:#0000ff;color:#ffffff;white-space:pre-wrap;" + "}</style><p>Text with</p>\n<pre><code>code\n</code></pre>\n"); +}