diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index edf2356c..4f76f667 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,17 +96,11 @@ jobs: with: distribution: 'zulu' java-version: '11' - - name: -- Step 4 -- Use Expo action - uses: expo/expo-github-action@v7 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - name: -- Step 5 -- Install node dependencies + - name: -- Step 4 -- Install node dependencies run: yarn install - - name: -- Step 6 -- Install ruby dependencies + - name: -- Step 5 -- Install ruby dependencies run: bundle install - - name: -- Step 7 -- Run fastlane + - name: -- Step 6 -- Run fastlane env: ENVIRONMENT: ${{ steps.branch.outputs.branch }} LC_ALL: en_US.UTF-8 @@ -118,9 +112,5 @@ jobs: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }} ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }} - GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }} FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }} run: yarn app:build release - - name: -- Step 8 -- Publish expo update - run: eas update --auto - diff --git a/VERSIONING.md b/VERSIONING.md deleted file mode 100644 index 7a500bd4..00000000 --- a/VERSIONING.md +++ /dev/null @@ -1,24 +0,0 @@ -## Major releases - App Store - -"Major releases" are artifacts published as `x.?.?`: - * An artifact must be released as `x.?.?` if native modules have been changed or updated, including upgrading Expo SDK version. - * A new app store version has to be submitted. - * Outdated versions in principle do not receive further OTA updates. - -## Minor releases - App Store - -"Minor releases" are artifacts published as `?.y.?`: - * An artifact can be released as `?.y.?` when there is no change nor update made to the native modules. - * A new app store version can be submitted for better first launch experience. - * All these versions that are not part of above mentioned outdates versions receive also OTA updates. - -## Patch releases - OTA - -"Patch releases" are artifacts published as `?.?.z`: - * An artifact must be release as `?.?.z` when there is no major change to the functionalities. - * No new app store version will be submitted. - * All these versions that are not part of above mentioned outdates versions receive also OTA updates. - -## OTA release channels - - * `MAJOR.MINOR-environment`. Environments include `release`, `candidate` and `development`. diff --git a/android/app/build.gradle b/android/app/build.gradle index fb8b1377..7908f03c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -170,10 +170,6 @@ android { } } } - manifestPlaceholders = [ - runtimeVersion: project.hasProperty('runtimeVersion') ? project.property('runtimeVersion') : "", - branch: project.hasProperty('branch') ? project.property('branch') : "development" - ] } if (isNewArchitectureEnabled()) { // We configure the NDK build only if you decide to opt-in for the New Architecture. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d6209e89..85094f05 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,12 +13,9 @@ - - - - - - + + + diff --git a/android/app/src/main/java/com/xmflsct/app/tooot/MainApplication.java b/android/app/src/main/java/com/xmflsct/app/tooot/MainApplication.java index 97c73b7a..df6bdbcf 100644 --- a/android/app/src/main/java/com/xmflsct/app/tooot/MainApplication.java +++ b/android/app/src/main/java/com/xmflsct/app/tooot/MainApplication.java @@ -16,7 +16,6 @@ import com.facebook.soloader.SoLoader; import expo.modules.ApplicationLifecycleDispatcher; import expo.modules.ReactNativeHostWrapper; -import expo.modules.updates.UpdatesController; import java.lang.reflect.InvocationTargetException; import java.util.List; diff --git a/app.config.ts b/app.config.ts index 53adc405..d7c2413b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,21 +1,15 @@ import { ExpoConfig } from '@expo/config' -import { versions } from './package.json' +import { version } from './package.json' import 'dotenv/config' -const toootVersion = `${versions.major}.${versions.minor}.${versions.patch}` - export default (): ExpoConfig => ({ - updates: { - url: "https://u.expo.dev/3288313f-3ff0-496a-a5a9-d8985e7cad5f" - }, - runtimeVersion: `${versions.major}.${versions.minor}`, name: 'tooot', description: 'tooot for Mastodon', slug: 'tooot', scheme: 'tooot', - version: toootVersion, + version, + extra: { environment: process.env.ENVIRONMENT }, privacy: 'hidden', - assetBundlePatterns: ['assets/*'], hooks: { postPublish: [ { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9791512c..232384bd 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,19 +1,10 @@ skip_docs -VERSIONS = read_json( json_path: "./package.json" )[:versions] +VERSION = read_json( json_path: "./package.json" )[:version] +GITHUB_RELEASE = "v#{VERSION}" ENVIRONMENT = ENV["ENVIRONMENT"] -VERSION = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}" -RELEASE_CHANNEL = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}-#{ENVIRONMENT}" BUILD_NUMBER = "#{Time.now.strftime("%y%m%d")}#{ENV["GITHUB_RUN_NUMBER"]}" GITHUB_REPO = "tooot-app/app" -case ENVIRONMENT -when "candidate" - GITHUB_RELEASE = "v#{VERSION}-#{VERSIONS[:patch]}" -when "release" - GITHUB_RELEASE = "v#{VERSION}" -else - GITHUB_RELEASE= "" -end XCODEPROJ = "./ios/tooot.xcodeproj" INFO_PLIST = "./ios/tooot/Info.plist" @@ -31,9 +22,6 @@ private_lane :build_ios do IPA_FILE = "#{BUILD_DIRECTORY}/tooot.ipa" DSYM_FILE = "#{BUILD_DIRECTORY}/tooot.app.dSYM.zip" - set_info_plist_value( path: EXPO_PLIST, key: "EXUpdatesRuntimeVersion", value: VERSION ) - set_info_plist_value( path: EXPO_PLIST, key: "EXUpdatesRequestHeaders", subkey: "expo-channel-name", value: ENVIRONMENT ) - setup_ci set_info_plist_value( path: INFO_PLIST, key: "CFBundleShortVersionString", value: VERSION ) set_info_plist_value( @@ -97,8 +85,6 @@ private_lane :build_android do print_command: true, print_command_output: true, properties: { - "runtimeVersion" => VERSION, - "branch" => ENVIRONMENT, "android.injected.signing.store.file" => "#{File.expand_path('..', Dir.pwd)}/android/tooot.jks", "android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], "android.injected.signing.key.alias" => ENV["ANDROID_KEYSTORE_ALIAS"], @@ -136,8 +122,6 @@ private_lane :build_android_apk do print_command: true, print_command_output: true, properties: { - "runtimeVersion" => VERSION, - "branch" => ENVIRONMENT, "android.injected.signing.store.file" => "#{File.expand_path('..', Dir.pwd)}/android/tooot.jks", "android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], "android.injected.signing.key.alias" => ENV["ANDROID_KEYSTORE_ALIAS"], @@ -149,31 +133,18 @@ private_lane :build_android_apk do end lane :ios do - releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_bearer: ENV['GH_PAT_GET_RELEASE']) - if releaseExists - puts("Release #{GITHUB_RELEASE} exists. Continue with building React Native only.") - else - puts("Release #{GITHUB_RELEASE} does not exist.") - cocoapods(clean_install: true, podfile: "./ios/Podfile") - build_ios - end + cocoapods(clean_install: true, podfile: "./ios/Podfile") + build_ios rocket end lane :android do - releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_bearer: ENV['GH_PAT_GET_RELEASE']) - if releaseExists - puts("Release #{GITHUB_RELEASE} exists. Continue with building React Native only.") - else - puts("Release #{GITHUB_RELEASE} does not exist. Create new release as well as new native build.") - build_android - end + build_android rocket end lane :release do - releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_bearer: ENV['GH_PAT_GET_RELEASE']) - if !releaseExists + if ENVIRONMENT == 'release' build_android_apk set_github_release( repository_name: GITHUB_REPO, @@ -181,7 +152,7 @@ lane :release do tag_name: GITHUB_RELEASE, description: "No changelog provided", commitish: git_branch, - is_prerelease: ENVIRONMENT == 'candidate', + is_prerelease: false, upload_assets: ["#{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk"] ) end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2d85efc..28466906 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -59,7 +59,7 @@ PODS: - EXStoreReview (5.3.0): - ExpoModulesCore - EXStructuredHeaders (2.2.1) - - EXUpdates (0.14.4): + - EXUpdates (0.14.5): - ASN1Decoder (~> 1.8) - EASClient - EXManifests @@ -141,24 +141,24 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/AppDelegateSwizzler (7.8.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.7.0): + - GoogleUtilities/Environment (7.8.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Logger (7.8.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/MethodSwizzler (7.8.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Network (7.8.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.7.0)" - - GoogleUtilities/Reachability (7.7.0): + - "GoogleUtilities/NSData+zlib (7.8.0)" + - GoogleUtilities/Reachability (7.8.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/UserDefaults (7.8.0): - GoogleUtilities/Logger - hermes-engine (0.69.4) - libevent (2.1.12) @@ -533,7 +533,7 @@ PODS: - React-Core - RNFastImage (8.5.11): - React-Core - - SDWebImage (~> 5.13.2) + - SDWebImage (~> 5.13.3) - SDWebImageWebPCoder (~> 0.9.0) - RNGestureHandler (2.5.0): - React-Core @@ -574,10 +574,10 @@ PODS: - React - RNSVG (13.0.0): - React-Core - - SDWebImage (5.13.2): - - SDWebImage/Core (= 5.13.2) - - SDWebImage/Core (5.13.2) - - SDWebImageWebPCoder (0.9.0): + - SDWebImage (5.13.3): + - SDWebImage/Core (= 5.13.3) + - SDWebImage/Core (5.13.3) + - SDWebImageWebPCoder (0.9.1): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.13) - Sentry (7.23.0): @@ -892,7 +892,7 @@ SPEC CHECKSUMS: EXSplashScreen: 31ab6df6d23e97e074d1330224741979943f1d82 EXStoreReview: cbb6b2202bb6f831cd3234d9d8b995cec0eb32f2 EXStructuredHeaders: 5d86829469399370a9fc7cb1e4391b09de87681d - EXUpdates: 784c8c593f6649d0640e81cede66613703e3c267 + EXUpdates: 664f999db423f90f3fb3d91edcda9bc28cb5a5b4 EXUpdatesInterface: 2bbc11815dfa2ec3fc02e5534c7592c6b42b5327 EXVideoThumbnails: 486533e1a66c9859f9b9e3b2e1f9f0b275515b48 FBLazyVector: c71b8c429a8af2aff1013934a7152e9d9d0c937d @@ -906,7 +906,7 @@ SPEC CHECKSUMS: glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a GoogleAppMeasurement: 71156240babd3cc6ced03e0d54816f01a880c730 GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f - GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7 hermes-engine: 761a544537e62df2a37189389b9d2654dc1f75af libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -952,15 +952,15 @@ SPEC CHECKSUMS: ReactCommon: 8f67bd7e0a6afade0f20718f859dc8c2275f2e83 RNCAsyncStorage: b2489b49e38c85e10ed45a888d13a2a4c7b32ea1 RNCClipboard: f1736c75ab85b627a4d57587edb4b60999c4dd80 - RNFastImage: 8e9b5b9e6df94d2e359c0a75a4745ad1311506fd + RNFastImage: d2efd2a7c06f15141b2c4116f7c0f14ee429f458 RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNReanimated: 2cf7451318bb9cc430abeec8d67693f9cf4e039c RNScreens: ee31ecdf23fe81e93c74feaa086cf173d758ab58 RNSentry: 7495ba091f09f12902d8cf916024efd99b058efe RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 RNSVG: 42a0c731b11179ebbd27a3eeeafa7201ebb476ff - SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 - SDWebImageWebPCoder: 3dc350894112feab5375cfba9ce0986544a66a69 + SDWebImage: af5bbffef2cde09f148d826f9733dcde1a9414cd + SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 Sentry: a0d4563fa4ddacba31fdcc35daaa8573d87224d6 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b Yoga: ff994563b2fd98c982ca58e8cd9db2cdaf4dda74 diff --git a/ios/tooot/Supporting/Expo.plist b/ios/tooot/Supporting/Expo.plist index 27a3a226..b4c81f20 100644 --- a/ios/tooot/Supporting/Expo.plist +++ b/ios/tooot/Supporting/Expo.plist @@ -2,20 +2,11 @@ - EXUpdatesCheckOnLaunch - WIFI_ONLY + EXUpdatesAutoSetup + EXUpdatesEnabled - - EXUpdatesLaunchWaitMs - 0 - EXUpdatesRequestHeaders - - expo-channel-name - development - - EXUpdatesRuntimeVersion - 0 - EXUpdatesURL - https://u.expo.dev/3288313f-3ff0-496a-a5a9-d8985e7cad5f + + EXUpdatesSDKVersion + 46.0.0 \ No newline at end of file diff --git a/metro.config.js b/metro.config.js index 22366d05..6b6f2944 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,6 +1,3 @@ module.exports = { - transformer: { - assetPlugins: ['expo-asset/tools/hashAssetFiles'], - inlineRequires: true - } + transformer: { inlineRequires: true } } diff --git a/package.json b/package.json index 4281b154..717f85a2 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,6 @@ { "name": "tooot", - "versions": { - "major": 4, - "minor": 3, - "patch": 1 - }, + "version": "4.3.2", "description": "tooot app for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", @@ -62,11 +58,12 @@ "expo-secure-store": "^11.3.0", "expo-splash-screen": "^0.16.1", "expo-store-review": "^5.3.0", - "expo-updates": "^0.14.4", + "expo-updates": "^0.14.5", "expo-video-thumbnails": "^6.4.0", "expo-web-browser": "^11.0.0", "i18next": "^21.9.1", "li": "^1.3.0", + "linkify-it": "^4.0.1", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -107,6 +104,7 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@expo/config": "^7.0.1", + "@types/linkify-it": "^3.0.2", "@types/lodash": "^4.14.184", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", diff --git a/patches/react-native-fast-image+8.5.11.patch b/patches/react-native-fast-image+8.5.11.patch index a2347146..1fdc8dc2 100644 --- a/patches/react-native-fast-image+8.5.11.patch +++ b/patches/react-native-fast-image+8.5.11.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-fast-image/RNFastImage.podspec b/node_modules/react-native-fast-image/RNFastImage.podspec -index db0fada..9379119 100644 +index db0fada..a869641 100644 --- a/node_modules/react-native-fast-image/RNFastImage.podspec +++ b/node_modules/react-native-fast-image/RNFastImage.podspec @@ -16,6 +16,6 @@ Pod::Spec.new do |s| @@ -8,7 +8,7 @@ index db0fada..9379119 100644 s.dependency 'React-Core' - s.dependency 'SDWebImage', '~> 5.11.1' - s.dependency 'SDWebImageWebPCoder', '~> 0.8.4' -+ s.dependency 'SDWebImage', '~> 5.13.2' ++ s.dependency 'SDWebImage', '~> 5.13.3' + s.dependency 'SDWebImageWebPCoder', '~> 0.9.0' end diff --git a/node_modules/react-native-fast-image/android/build.gradle b/node_modules/react-native-fast-image/android/build.gradle diff --git a/src/App.tsx b/src/App.tsx index 6419e708..788008db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,6 @@ import { getSettingsLanguage } from '@utils/slices/settingsSlice' import ThemeManager from '@utils/styles/ThemeManager' -import 'expo-asset' import * as SplashScreen from 'expo-splash-screen' import React, { useCallback, useEffect, useState } from 'react' import { LogBox, Platform } from 'react-native' diff --git a/src/Screens.tsx b/src/Screens.tsx index 07a072be..4bd00d8c 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -251,7 +251,6 @@ const Screens: React.FC = ({ localCorrupt }) => { if (!text && !media.length) { return } else { - console.log('share', text, media) if (instances.length > 1) { navigationRef.navigate('Screen-AccountSelection', { share: { text, media } diff --git a/src/components/Emojis.tsx b/src/components/Emojis.tsx index 044dce6d..755d2b84 100644 --- a/src/components/Emojis.tsx +++ b/src/components/Emojis.tsx @@ -4,19 +4,13 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useEmojisQuery } from '@utils/queryHooks/emojis' import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice' import { chunk, forEach, groupBy, sortBy } from 'lodash' -import React, { - Dispatch, - MutableRefObject, - PropsWithChildren, - SetStateAction, - useCallback, - useEffect, - useReducer -} from 'react' +import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Keyboard, KeyboardAvoidingView, View } from 'react-native' import FastImage from 'react-native-fast-image' +import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useSelector } from 'react-redux' -import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext' +import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Emojis/helpers/EmojisContext' const prefetchEmojis = ( sortedEmojis: { @@ -45,81 +39,37 @@ const prefetchEmojis = ( } catch {} } -export interface Props { - enabled?: boolean - value?: string - setValue: - | Dispatch> - | Dispatch> - selectionRange: MutableRefObject<{ - start: number - end: number - }> - maxLength?: number +export type Props = { + inputProps: EmojisState['inputProps'] + customButton?: boolean + customEdges?: Edge[] + customBehavior?: 'height' | 'padding' | 'position' } +export const emojis: Emojis = createRef() + const ComponentEmojis: React.FC = ({ - enabled = false, - value, - setValue, - selectionRange, - maxLength, - children + children, + inputProps, + customButton = false, + customEdges = ['bottom'], + customBehavior }) => { const { reduceMotionEnabled } = useAccessibility() - const [emojisState, emojisDispatch] = useReducer(emojisReducer, { - enabled, - active: false, - emojis: [], - shortcode: null - }) - + const [emojisState, emojisDispatch] = useReducer(emojisReducer, { inputProps, targetIndex: -1 }) useEffect(() => { - if (emojisState.shortcode) { - addEmoji(emojisState.shortcode) - emojisDispatch({ - type: 'shortcode', - payload: null - }) - } - }, [emojisState.shortcode]) - - const addEmoji = useCallback( - (emojiShortcode: string) => { - if (value?.length) { - const contentFront = value.slice(0, selectionRange.current?.start) - const contentRear = value.slice(selectionRange.current?.end) - - const whiteSpaceRear = /\s/g.test(contentRear.slice(-1)) - - const newTextWithSpace = ` ${emojiShortcode}${ - whiteSpaceRear ? '' : ' ' - }` - setValue( - [contentFront, newTextWithSpace, contentRear] - .join('') - .slice(0, maxLength) - ) - } else { - setValue(`${emojiShortcode} `.slice(0, maxLength)) - } - }, - [value, selectionRange.current?.start, selectionRange.current?.end] - ) + emojisDispatch({ type: 'input', payload: inputProps }) + }, [inputProps]) const { t } = useTranslation() - const { data } = useEmojisQuery({ options: { enabled } }) + const { data } = useEmojisQuery({}) const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true) useEffect(() => { if (data && data.length) { - let sortedEmojis: { - title: string - data: Pick[][] - }[] = [] - forEach( - groupBy(sortBy(data, ['category', 'shortcode']), 'category'), - (value, key) => sortedEmojis.push({ title: key, data: chunk(value, 5) }) + let sortedEmojis: NonNullable = [] + forEach(groupBy(sortBy(data, ['category', 'shortcode']), 'category'), (value, key) => + sortedEmojis.push({ title: key, data: chunk(value, 5) }) ) if (frequentEmojis.length) { sortedEmojis.unshift({ @@ -127,22 +77,60 @@ const ComponentEmojis: React.FC = ({ data: chunk( frequentEmojis.map(e => e.emoji), 5 - ) + ), + type: 'frequent' }) } - emojisDispatch({ - type: 'load', - payload: sortedEmojis - }) + emojis.current = sortedEmojis prefetchEmojis(sortedEmojis, reduceMotionEnabled) } }, [data, reduceMotionEnabled]) + const insets = useSafeAreaInsets() + const [keyboardShown, setKeyboardShown] = useState(false) + useEffect(() => { + const showSubscription = Keyboard.addListener('keyboardWillShow', () => { + const anyInputHasFocus = inputProps.filter(props => props.isFocused.current).length + if (anyInputHasFocus) { + emojisDispatch({ type: 'target', payload: -1 }) + } + setKeyboardShown(true) + }) + const hideSubscription = Keyboard.addListener('keyboardWillHide', () => { + setKeyboardShown(false) + }) + + return () => { + showSubscription.remove() + hideSubscription.remove() + } + }, [inputProps]) + return ( - + + + + + {children} + + ) : customButton ? null : ( + + ) + } + /> + + + + ) } diff --git a/src/components/Emojis/Button.tsx b/src/components/Emojis/Button.tsx index 92ce4620..c7783541 100644 --- a/src/components/Emojis/Button.tsx +++ b/src/components/Emojis/Button.tsx @@ -1,50 +1,54 @@ +import { emojis } from '@components/Emojis' import Icon from '@components/Icon' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext } from 'react' -import { Pressable, StyleSheet } from 'react-native' +import { Keyboard, Pressable, View } from 'react-native' import EmojisContext from './helpers/EmojisContext' -const EmojisButton = React.memo( - () => { - const { colors } = useTheme() - const { emojisState, emojisDispatch } = useContext(EmojisContext) +const EmojisButton: React.FC = () => { + const { colors } = useTheme() + const { emojisState, emojisDispatch } = useContext(EmojisContext) - return emojisState.enabled ? ( - - emojisDispatch({ type: 'activate', payload: !emojisState.active }) + const focusedPropsIndex = emojisState.inputProps?.findIndex(props => props.isFocused.current) + if (focusedPropsIndex === -1) { + return null + } + + return ( + { + if (emojisState.targetIndex === -1) { + Keyboard.dismiss() } - hitSlop={StyleConstants.Spacing.S} - style={styles.base} - children={ + emojisDispatch({ type: 'target', payload: focusedPropsIndex }) + }} + hitSlop={StyleConstants.Spacing.S} + style={{ + alignSelf: 'flex-end', + padding: StyleConstants.Spacing.Global.PagePadding / 2 + }} + children={ + - } - /> - ) : null - }, - () => true -) - -const styles = StyleSheet.create({ - base: { - paddingLeft: StyleConstants.Spacing.S - } -}) + + } + /> + ) +} export default EmojisButton diff --git a/src/components/Emojis/List.tsx b/src/components/Emojis/List.tsx index 32a2a931..0ccb2c55 100644 --- a/src/components/Emojis/List.tsx +++ b/src/components/Emojis/List.tsx @@ -1,3 +1,5 @@ +import { emojis } from '@components/Emojis' +import Icon from '@components/Icon' import CustomText from '@components/Text' import { useAppDispatch } from '@root/store' import { useAccessibility } from '@utils/accessibility/AccessibilityManager' @@ -5,115 +7,215 @@ import { countInstanceEmoji } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useContext, useEffect, useRef } from 'react' +import { chunk } from 'lodash' +import React, { useContext, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { AccessibilityInfo, findNodeHandle, Pressable, SectionList, + TextInput, View } from 'react-native' import FastImage from 'react-native-fast-image' import validUrl from 'valid-url' import EmojisContext from './helpers/EmojisContext' -const EmojisList = React.memo( - () => { - const dispatch = useAppDispatch() - const { reduceMotionEnabled } = useAccessibility() - const { t } = useTranslation() +const EmojisList = () => { + const dispatch = useAppDispatch() + const { reduceMotionEnabled } = useAccessibility() + const { t } = useTranslation() - const { emojisState, emojisDispatch } = useContext(EmojisContext) - const { colors } = useTheme() + const { emojisState, emojisDispatch } = useContext(EmojisContext) + const { colors, mode } = useTheme() - const listItem = useCallback( - ({ index, item }: { item: Mastodon.Emoji[]; index: number }) => { - return ( - - {item.map(emoji => { - const uri = reduceMotionEnabled ? emoji.static_url : emoji.url - if (validUrl.isHttpsUri(uri)) { - return ( - { - emojisDispatch({ - type: 'shortcode', - payload: `:${emoji.shortcode}:` - }) - dispatch(countInstanceEmoji(emoji)) - }} - > - - - ) - } else { - return null - } - })} - - ) - }, - [] + const addEmoji = (shortcode: string) => { + if (emojisState.targetIndex === -1) { + return + } + + const { + value: [value, setValue], + selection: [selection, setSelection], + ref, + maxLength + } = emojisState.inputProps[emojisState.targetIndex] + + const contentFront = value.slice(0, selection.start) + const contentRear = value.slice(selection.end || selection.start) + + const spaceFront = value.length === 0 || /\s/g.test(contentFront.slice(-1)) ? '' : ' ' + const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' ' + + setValue( + [contentFront, spaceFront, shortcode, spaceRear, contentRear].join('').slice(0, maxLength) ) - const listRef = useRef(null) - useEffect(() => { - layoutAnimation() - const tagEmojis = findNodeHandle(listRef.current) - if (emojisState.active) { - tagEmojis && AccessibilityInfo.setAccessibilityFocus(tagEmojis) - } - }, [emojisState.active]) + const addedLength = spaceFront.length + shortcode.length + spaceRear.length + setSelection({ start: selection.start + addedLength }) + ref?.current?.setNativeProps({ + selection: { start: selection.start + addedLength } + }) + } - return emojisState.active ? ( + const listItem = ({ index, item }: { item: Mastodon.Emoji[]; index: number }) => { + return ( + + {item.map(emoji => { + const uri = reduceMotionEnabled ? emoji.static_url : emoji.url + if (validUrl.isHttpsUri(uri)) { + return ( + { + addEmoji(`:${emoji.shortcode}:`) + dispatch(countInstanceEmoji(emoji)) + }} + style={{ padding: StyleConstants.Spacing.S }} + > + + + ) + } else { + return null + } + })} + + ) + } + + const listRef = useRef(null) + useEffect(() => { + const tagEmojis = findNodeHandle(listRef.current) + if (emojisState.targetIndex !== -1) { + layoutAnimation() + tagEmojis && AccessibilityInfo.setAccessibilityFocus(tagEmojis) + } + }, [emojisState.targetIndex]) + + const [search, setSearch] = useState('') + const searchLength = useRef(0) + useEffect(() => { + if ( + (search.length === 0 && searchLength.current === 1) || + (search.length === 1 && searchLength.current === 0) + ) { + layoutAnimation() + } + searchLength.current = search.length + }, [search.length, searchLength.current]) + + return emojisState.targetIndex !== -1 ? ( + + + + + + + { + if (emojisState.targetIndex !== -1) { + emojisState.inputProps[emojisState.targetIndex].ref?.current?.focus() + } + emojisDispatch({ type: 'target', payload: -1 }) + }} + > + + + item[0].shortcode} + sections={ + search.length + ? [ + { + title: 'Search result', + data: emojis.current + ? chunk( + emojis.current + .filter(e => e.type !== 'frequent') + .flatMap(e => + e.data.flatMap(e => e).filter(emoji => emoji.shortcode.includes(search)) + ), + 2 + ) + : [] + } + ] + : emojis.current || [] + } + keyExtractor={item => item[0]?.shortcode} renderSectionHeader={({ section: { title } }) => ( - + {title} )} renderItem={listItem} windowSize={4} + contentContainerStyle={{ + paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, + minHeight: 32 * 2 + StyleConstants.Spacing.M * 3 + }} /> - ) : null - }, - () => true -) + + ) : null +} export default EmojisList diff --git a/src/components/Emojis/helpers/EmojisContext.tsx b/src/components/Emojis/helpers/EmojisContext.tsx index 121207de..c7a80e33 100644 --- a/src/components/Emojis/helpers/EmojisContext.tsx +++ b/src/components/Emojis/helpers/EmojisContext.tsx @@ -1,28 +1,31 @@ -import { createContext, Dispatch } from 'react' +import { createContext, Dispatch, MutableRefObject, RefObject } from 'react' +import { TextInput } from 'react-native' + +type inputProps = { + value: [string, (value: string) => void] + selection: [{ start: number; end?: number }, (selection: { start: number; end?: number }) => void] + isFocused: MutableRefObject + ref: RefObject // For controlling focus + maxLength?: number +} + +export type Emojis = MutableRefObject< + | { + title: string + data: Pick[][] + type?: 'frequent' + }[] + | null +> export type EmojisState = { - enabled: boolean - active: boolean - emojis: { - title: string - data: Pick[][] - }[] - shortcode: Mastodon.Emoji['shortcode'] | null + inputProps: inputProps[] + targetIndex: number } export type EmojisAction = - | { - type: 'load' - payload: NonNullable - } - | { - type: 'activate' - payload: EmojisState['active'] - } - | { - type: 'shortcode' - payload: EmojisState['shortcode'] - } + | { type: 'input'; payload: EmojisState['inputProps'] } + | { type: 'target'; payload: EmojisState['targetIndex'] } type ContextType = { emojisState: EmojisState @@ -32,12 +35,10 @@ const EmojisContext = createContext({} as ContextType) export const emojisReducer = (state: EmojisState, action: EmojisAction) => { switch (action.type) { - case 'activate': - return { ...state, active: action.payload } - case 'load': - return { ...state, emojis: action.payload } - case 'shortcode': - return { ...state, shortcode: action.payload } + case 'input': + return { ...state, inputProps: action.payload } + case 'target': + return { ...state, targetIndex: action.payload } } } diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 5c3752c2..7354dc44 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,99 +1,62 @@ import { StyleConstants } from '@utils/styles/constants' -import layoutAnimation from '@utils/styles/layoutAnimation' import { useTheme } from '@utils/styles/ThemeManager' -import React, { - Dispatch, - SetStateAction, - useEffect, - useRef, - useState -} from 'react' +import React, { forwardRef, RefObject } from 'react' import { Platform, TextInput, TextInputProps, View } from 'react-native' import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' -import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis' -import EmojisContext from './Emojis/helpers/EmojisContext' +import { EmojisState } from './Emojis/helpers/EmojisContext' import CustomText from './Text' -export interface Props { - autoFocus?: boolean - +export type Props = { title?: string - multiline?: boolean - - emoji?: boolean - - value?: string - setValue: - | Dispatch> - | Dispatch> - - options?: Omit< +} & Pick, 'value' | 'selection' | 'isFocused'> & + Omit< TextInputProps, - | 'autoFocus' - | 'onFocus' - | 'onBlur' | 'style' | 'onChangeText' | 'onSelectionChange' | 'keyboardAppearance' | 'textAlignVertical' + | 'multiline' + | 'selection' + | 'value' > -} -const Input: React.FC = ({ - autoFocus = true, - title, - multiline = false, - emoji = false, - value, - setValue, - options -}) => { - const { colors, mode } = useTheme() +const ComponentInput = forwardRef( + ( + { + title, + multiline = false, + value: [value, setValue], + selection: [selection, setSelection], + isFocused, + ...props + }: Props, + ref: RefObject + ) => { + const { colors, mode } = useTheme() - const animateTitle = useAnimatedStyle(() => { - if (value) { - return { - fontSize: withTiming(StyleConstants.Font.Size.S), - paddingHorizontal: withTiming(StyleConstants.Spacing.XS), - left: withTiming(StyleConstants.Spacing.S), - top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2), - backgroundColor: withTiming(colors.backgroundDefault) - } - } else { - return { - fontSize: withTiming(StyleConstants.Font.Size.M), - paddingHorizontal: withTiming(0), - left: withTiming(StyleConstants.Spacing.S), - top: withTiming(StyleConstants.Spacing.S + 1), - backgroundColor: withTiming(colors.backgroundDefaultTransparent) - } - } - }, [mode, value]) - - const selectionRange = useRef<{ start: number; end: number }>( - value - ? { - start: value.length, - end: value.length + const animateTitle = useAnimatedStyle(() => { + if (value) { + return { + fontSize: withTiming(StyleConstants.Font.Size.S), + paddingHorizontal: withTiming(StyleConstants.Spacing.XS), + left: withTiming(StyleConstants.Spacing.S), + top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2), + backgroundColor: withTiming(colors.backgroundDefault) } - : { start: 0, end: 0 } - ) + } else { + return { + fontSize: withTiming(StyleConstants.Font.Size.M), + paddingHorizontal: withTiming(0), + left: withTiming(StyleConstants.Spacing.S), + top: withTiming(StyleConstants.Spacing.S + 1), + backgroundColor: withTiming(colors.backgroundDefaultTransparent) + } + } + }, [mode, value]) - const [inputFocused, setInputFocused] = useState(false) - useEffect(() => { - layoutAnimation() - }, [inputFocused]) - - return ( - + return ( = ({ alignItems: 'stretch' }} > - - {({ emojisDispatch }) => ( - setInputFocused(true)} - onBlur={() => { - setInputFocused(false) - emojisDispatch({ type: 'activate', payload: false }) - }} - style={{ - flex: 1, - fontSize: StyleConstants.Font.Size.M, - color: colors.primaryDefault, - minHeight: - Platform.OS === 'ios' && multiline - ? StyleConstants.Font.LineHeight.M * 5 - : undefined - }} - onChangeText={setValue} - onSelectionChange={({ nativeEvent: { selection } }) => - (selectionRange.current = selection) - } - value={value} - {...(multiline && { - multiline, - numberOfLines: Platform.OS === 'android' ? 5 : undefined - })} - keyboardAppearance={mode} - textAlignVertical='top' - {...options} - /> - )} - + (isFocused.current = true)} + onBlur={() => (isFocused.current = false)} + onSelectionChange={({ nativeEvent }) => setSelection(nativeEvent.selection)} + {...(multiline && { + multiline, + numberOfLines: Platform.OS === 'android' ? 5 : undefined + })} + keyboardAppearance={mode} + textAlignVertical='top' + {...props} + /> + {title ? ( - + {title} ) : null} + - {options?.maxLength && value?.length ? ( + {props?.maxLength && value?.length ? ( = ({ color: colors.secondary }} > - {value?.length} / {options.maxLength} + {value?.length} / {props.maxLength} ) : null} - {inputFocused ? : null} - - - ) -} + ) + } +) -export default Input +export default ComponentInput diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index d6097f0d..e5027ff1 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -50,10 +50,7 @@ const MenuRow: React.FC = ({ const loadingSpinkit = useMemo( () => ( - + ), [theme] @@ -111,11 +108,7 @@ const MenuRow: React.FC = ({ }} /> ) : null} - + {title} @@ -127,7 +120,7 @@ const MenuRow: React.FC = ({ flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', - marginLeft: StyleConstants.Spacing.M + paddingLeft: StyleConstants.Spacing.L }} > {content ? ( diff --git a/src/components/Timeline/Shared/Attachment.tsx b/src/components/Timeline/Shared/Attachment.tsx index 02857181..ba9fad2a 100644 --- a/src/components/Timeline/Shared/Attachment.tsx +++ b/src/components/Timeline/Shared/Attachment.tsx @@ -41,6 +41,7 @@ const TimelineAttachment = React.memo( } const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) + // @ts-ignore const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments .map(attachment => { diff --git a/src/i18n/de/components/contextMenu.json b/src/i18n/de/components/contextMenu.json index d83f947b..808a5d0f 100644 --- a/src/i18n/de/components/contextMenu.json +++ b/src/i18n/de/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "" }, "reports": { - "action": "User melden" + "action": "Melden und blockieren" } }, "copy": { diff --git a/src/i18n/en/components/contextMenu.json b/src/i18n/en/components/contextMenu.json index d7ba0c11..6dbd8ea1 100644 --- a/src/i18n/en/components/contextMenu.json +++ b/src/i18n/en/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "Unblock user" }, "reports": { - "action": "Report user" + "action": "Report and block" } }, "copy": { diff --git a/src/i18n/it/components/contextMenu.json b/src/i18n/it/components/contextMenu.json index abfcd532..6f130c39 100644 --- a/src/i18n/it/components/contextMenu.json +++ b/src/i18n/it/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "Sblocca utente" }, "reports": { - "action": "Segnala utente" + "action": "Segnala e blocca" } }, "copy": { diff --git a/src/i18n/ja/components/contextMenu.json b/src/i18n/ja/components/contextMenu.json index 95dbc129..47a35d43 100644 --- a/src/i18n/ja/components/contextMenu.json +++ b/src/i18n/ja/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "ユーザーのブロックを解除" }, "reports": { - "action": "ユーザーを報告" + "action": "通報とブロック" } }, "copy": { diff --git a/src/i18n/ko/components/contextMenu.json b/src/i18n/ko/components/contextMenu.json index 9f88f338..063b227b 100644 --- a/src/i18n/ko/components/contextMenu.json +++ b/src/i18n/ko/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "" }, "reports": { - "action": "사용자 신고" + "action": "" } }, "copy": { diff --git a/src/i18n/pt_BR/components/contextMenu.json b/src/i18n/pt_BR/components/contextMenu.json index 82634dde..0b1c7410 100644 --- a/src/i18n/pt_BR/components/contextMenu.json +++ b/src/i18n/pt_BR/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "Desbloquear usuário" }, "reports": { - "action": "Denunciar usuário" + "action": "Denunciar e bloquear" } }, "copy": { diff --git a/src/i18n/vi/components/contextMenu.json b/src/i18n/vi/components/contextMenu.json index 9e265115..0dee5c99 100644 --- a/src/i18n/vi/components/contextMenu.json +++ b/src/i18n/vi/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "Bỏ chặn người dùng" }, "reports": { - "action": "Báo cáo" + "action": "Báo cáo và chặn" } }, "copy": { diff --git a/src/i18n/zh-Hans/components/contextMenu.json b/src/i18n/zh-Hans/components/contextMenu.json index cd6339a6..a17add00 100644 --- a/src/i18n/zh-Hans/components/contextMenu.json +++ b/src/i18n/zh-Hans/components/contextMenu.json @@ -11,7 +11,7 @@ "action_true": "取消屏蔽用户" }, "reports": { - "action": "举报用户" + "action": "举报并屏蔽" } }, "copy": { diff --git a/src/modules/autolinker/anchor-tag-builder.d.ts b/src/modules/autolinker/anchor-tag-builder.d.ts deleted file mode 100644 index 09a5965d..00000000 --- a/src/modules/autolinker/anchor-tag-builder.d.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Match } from "./match/match"; -import { HtmlTag } from "./html-tag"; -import { TruncateConfigObj } from "./autolinker"; -/** - * @protected - * @class Autolinker.AnchorTagBuilder - * @extends Object - * - * Builds anchor (<a>) tags for the Autolinker utility when a match is - * found. - * - * Normally this class is instantiated, configured, and used internally by an - * {@link Autolinker} instance, but may actually be used indirectly in a - * {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} - * instances which may be modified before returning from the - * {@link Autolinker#replaceFn replaceFn}. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( match ) { - * var tag = match.buildTag(); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test google.com - */ -export declare class AnchorTagBuilder { - /** - * @cfg {Boolean} newWindow - * @inheritdoc Autolinker#newWindow - */ - private readonly newWindow; - /** - * @cfg {Object} truncate - * @inheritdoc Autolinker#truncate - */ - private readonly truncate; - /** - * @cfg {String} className - * @inheritdoc Autolinker#className - */ - private readonly className; - /** - * @method constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). - */ - constructor(cfg?: AnchorTagBuilderCfg); - /** - * Generates the actual anchor (<a>) tag to use in place of the - * matched text, via its `match` object. - * - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. - */ - build(match: Match): HtmlTag; - /** - * Creates the Object (map) of the HTML attributes for the anchor (<a>) - * tag being generated. - * - * @protected - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Object} A key/value Object (map) of the anchor tag's attributes. - */ - protected createAttrs(match: Match): { - [attrName: string]: string; - }; - /** - * Creates the CSS class that will be used for a given anchor tag, based on - * the `matchType` and the {@link #className} config. - * - * Example returns: - * - * - "" // no {@link #className} - * - "myLink myLink-url" // url match - * - "myLink myLink-email" // email match - * - "myLink myLink-phone" // phone match - * - "myLink myLink-hashtag" // hashtag match - * - "myLink myLink-mention myLink-twitter" // mention match with Twitter service - * - * @protected - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {String} The CSS class string for the link. Example return: - * "myLink myLink-url". If no {@link #className} was configured, returns - * an empty string. - */ - protected createCssClass(match: Match): string; - /** - * Processes the `anchorText` by truncating the text according to the - * {@link #truncate} config. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The processed `anchorText`. - */ - private processAnchorText; - /** - * Performs the truncation of the `anchorText` based on the {@link #truncate} - * option. If the `anchorText` is longer than the length specified by the - * {@link #truncate} option, the truncation is performed based on the - * `location` property. See {@link #truncate} for details. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The truncated anchor text. - */ - private doTruncate; -} -export interface AnchorTagBuilderCfg { - newWindow?: boolean; - truncate?: TruncateConfigObj; - className?: string; -} diff --git a/src/modules/autolinker/anchor-tag-builder.js b/src/modules/autolinker/anchor-tag-builder.js deleted file mode 100644 index b44caaab..00000000 --- a/src/modules/autolinker/anchor-tag-builder.js +++ /dev/null @@ -1,176 +0,0 @@ -import { HtmlTag } from "./html-tag"; -import { truncateSmart } from "./truncate/truncate-smart"; -import { truncateMiddle } from "./truncate/truncate-middle"; -import { truncateEnd } from "./truncate/truncate-end"; -/** - * @protected - * @class Autolinker.AnchorTagBuilder - * @extends Object - * - * Builds anchor (<a>) tags for the Autolinker utility when a match is - * found. - * - * Normally this class is instantiated, configured, and used internally by an - * {@link Autolinker} instance, but may actually be used indirectly in a - * {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} - * instances which may be modified before returning from the - * {@link Autolinker#replaceFn replaceFn}. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( match ) { - * var tag = match.buildTag(); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test google.com - */ -var AnchorTagBuilder = /** @class */ (function () { - /** - * @method constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). - */ - function AnchorTagBuilder(cfg) { - if (cfg === void 0) { cfg = {}; } - /** - * @cfg {Boolean} newWindow - * @inheritdoc Autolinker#newWindow - */ - this.newWindow = false; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Object} truncate - * @inheritdoc Autolinker#truncate - */ - this.truncate = {}; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {String} className - * @inheritdoc Autolinker#className - */ - this.className = ''; // default value just to get the above doc comment in the ES5 output and documentation generator - this.newWindow = cfg.newWindow || false; - this.truncate = cfg.truncate || {}; - this.className = cfg.className || ''; - } - /** - * Generates the actual anchor (<a>) tag to use in place of the - * matched text, via its `match` object. - * - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. - */ - AnchorTagBuilder.prototype.build = function (match) { - return new HtmlTag({ - tagName: 'a', - attrs: this.createAttrs(match), - innerHtml: this.processAnchorText(match.getAnchorText()) - }); - }; - /** - * Creates the Object (map) of the HTML attributes for the anchor (<a>) - * tag being generated. - * - * @protected - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Object} A key/value Object (map) of the anchor tag's attributes. - */ - AnchorTagBuilder.prototype.createAttrs = function (match) { - var attrs = { - 'href': match.getAnchorHref() // we'll always have the `href` attribute - }; - var cssClass = this.createCssClass(match); - if (cssClass) { - attrs['class'] = cssClass; - } - if (this.newWindow) { - attrs['target'] = "_blank"; - attrs['rel'] = "noopener noreferrer"; // Issue #149. See https://mathiasbynens.github.io/rel-noopener/ - } - if (this.truncate) { - if (this.truncate.length && this.truncate.length < match.getAnchorText().length) { - attrs['title'] = match.getAnchorHref(); - } - } - return attrs; - }; - /** - * Creates the CSS class that will be used for a given anchor tag, based on - * the `matchType` and the {@link #className} config. - * - * Example returns: - * - * - "" // no {@link #className} - * - "myLink myLink-url" // url match - * - "myLink myLink-email" // email match - * - "myLink myLink-phone" // phone match - * - "myLink myLink-hashtag" // hashtag match - * - "myLink myLink-mention myLink-twitter" // mention match with Twitter service - * - * @protected - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {String} The CSS class string for the link. Example return: - * "myLink myLink-url". If no {@link #className} was configured, returns - * an empty string. - */ - AnchorTagBuilder.prototype.createCssClass = function (match) { - var className = this.className; - if (!className) { - return ""; - } - else { - var returnClasses = [className], cssClassSuffixes = match.getCssClassSuffixes(); - for (var i = 0, len = cssClassSuffixes.length; i < len; i++) { - returnClasses.push(className + '-' + cssClassSuffixes[i]); - } - return returnClasses.join(' '); - } - }; - /** - * Processes the `anchorText` by truncating the text according to the - * {@link #truncate} config. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The processed `anchorText`. - */ - AnchorTagBuilder.prototype.processAnchorText = function (anchorText) { - anchorText = this.doTruncate(anchorText); - return anchorText; - }; - /** - * Performs the truncation of the `anchorText` based on the {@link #truncate} - * option. If the `anchorText` is longer than the length specified by the - * {@link #truncate} option, the truncation is performed based on the - * `location` property. See {@link #truncate} for details. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The truncated anchor text. - */ - AnchorTagBuilder.prototype.doTruncate = function (anchorText) { - var truncate = this.truncate; - if (!truncate || !truncate.length) - return anchorText; - var truncateLength = truncate.length, truncateLocation = truncate.location; - if (truncateLocation === 'smart') { - return truncateSmart(anchorText, truncateLength); - } - else if (truncateLocation === 'middle') { - return truncateMiddle(anchorText, truncateLength); - } - else { - return truncateEnd(anchorText, truncateLength); - } - }; - return AnchorTagBuilder; -}()); -export { AnchorTagBuilder }; - -//# sourceMappingURL=anchor-tag-builder.js.map diff --git a/src/modules/autolinker/autolinker.d.ts b/src/modules/autolinker/autolinker.d.ts deleted file mode 100644 index 979a7356..00000000 --- a/src/modules/autolinker/autolinker.d.ts +++ /dev/null @@ -1,699 +0,0 @@ -import { AnchorTagBuilder } from "./anchor-tag-builder"; -import { Match } from "./match/match"; -import { EmailMatch } from "./match/email-match"; -import { HashtagMatch } from "./match/hashtag-match"; -import { MentionMatch } from "./match/mention-match"; -import { PhoneMatch } from "./match/phone-match"; -import { UrlMatch } from "./match/url-match"; -import { Matcher } from "./matcher/matcher"; -import { HtmlTag } from "./html-tag"; -import { EmailMatcher } from "./matcher/email-matcher"; -import { UrlMatcher } from "./matcher/url-matcher"; -import { HashtagMatcher } from "./matcher/hashtag-matcher"; -import { PhoneMatcher } from "./matcher/phone-matcher"; -import { MentionMatcher } from "./matcher/mention-matcher"; -/** - * @class Autolinker - * @extends Object - * - * Utility class used to process a given string of text, and wrap the matches in - * the appropriate anchor (<a>) tags to turn them into links. - * - * Any of the configuration options may be provided in an Object provided - * to the Autolinker constructor, which will configure how the {@link #link link()} - * method will process the links. - * - * For example: - * - * var autolinker = new Autolinker( { - * newWindow : false, - * truncate : 30 - * } ); - * - * var html = autolinker.link( "Joe went to www.yahoo.com" ); - * // produces: 'Joe went to yahoo.com' - * - * - * The {@link #static-link static link()} method may also be used to inline - * options into a single call, which may be more convenient for one-off uses. - * For example: - * - * var html = Autolinker.link( "Joe went to www.yahoo.com", { - * newWindow : false, - * truncate : 30 - * } ); - * // produces: 'Joe went to yahoo.com' - * - * - * ## Custom Replacements of Links - * - * If the configuration options do not provide enough flexibility, a {@link #replaceFn} - * may be provided to fully customize the output of Autolinker. This function is - * called once for each URL/Email/Phone#/Hashtag/Mention (Twitter, Instagram, Soundcloud) - * match that is encountered. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, Phone #s, Hashtags, and Mentions (Twitter, Instagram, Soundcloud) - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { - * var tag = match.buildTag(); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes - * tag.setAttr( 'rel', 'nofollow' ); - * tag.addClass( 'external-link' ); - * - * return tag; - * - * } else { - * return true; // let Autolinker perform its normal anchor tag replacement - * } - * - * case 'email' : - * var email = match.getEmail(); - * console.log( "email: ", email ); - * - * if( email === "my@own.address" ) { - * return false; // don't auto-link this particular email address; leave as-is - * } else { - * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) - * } - * - * case 'phone' : - * var phoneNumber = match.getPhoneNumber(); - * console.log( phoneNumber ); - * - * return '' + phoneNumber + ''; - * - * case 'hashtag' : - * var hashtag = match.getHashtag(); - * console.log( hashtag ); - * - * return '' + hashtag + ''; - * - * case 'mention' : - * var mention = match.getMention(); - * console.log( mention ); - * - * return '' + mention + ''; - * } - * } - * } ); - * - * - * The function may return the following values: - * - * - `true` (Boolean): Allow Autolinker to replace the match as it normally - * would. - * - `false` (Boolean): Do not replace the current match at all - leave as-is. - * - Any String: If a string is returned from the function, the string will be - * used directly as the replacement HTML for the match. - * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify - * an HTML tag before writing out its HTML text. - */ -export default class Autolinker { - /** - * @static - * @property {String} version - * - * The Autolinker version number in the form major.minor.patch - * - * Ex: 0.25.1 - */ - static readonly version = "3.14.1"; - /** - * For backwards compatibility with Autolinker 1.x, the AnchorTagBuilder - * class is provided as a static on the Autolinker class. - */ - static readonly AnchorTagBuilder: typeof AnchorTagBuilder; - /** - * For backwards compatibility with Autolinker 1.x, the HtmlTag class is - * provided as a static on the Autolinker class. - */ - static readonly HtmlTag: typeof HtmlTag; - /** - * For backwards compatibility with Autolinker 1.x, the Matcher classes are - * provided as statics on the Autolinker class. - */ - static readonly matcher: { - Email: typeof EmailMatcher; - Hashtag: typeof HashtagMatcher; - Matcher: typeof Matcher; - Mention: typeof MentionMatcher; - Phone: typeof PhoneMatcher; - Url: typeof UrlMatcher; - }; - /** - * For backwards compatibility with Autolinker 1.x, the Match classes are - * provided as statics on the Autolinker class. - */ - static readonly match: { - Email: typeof EmailMatch; - Hashtag: typeof HashtagMatch; - Match: typeof Match; - Mention: typeof MentionMatch; - Phone: typeof PhoneMatch; - Url: typeof UrlMatch; - }; - /** - * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, - * Hashtags, and Mentions found in the given chunk of HTML. Does not link URLs - * found within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * Example: - * - * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); - * // Produces: "Go to google.com" - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within (depending - * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #mention}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {String} The HTML text, with matches automatically linked. - */ - static link(textOrHtml: string, options?: AutolinkerConfig): string; - /** - * Parses the input `textOrHtml` looking for URLs, email addresses, phone - * numbers, username handles, and hashtags (depending on the configuration - * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} - * objects describing those matches (without making any replacements). - * - * Note that if parsing multiple pieces of text, it is slightly more efficient - * to create an Autolinker instance, and use the instance-level {@link #parse} - * method. - * - * Example: - * - * var matches = Autolinker.parse( "Hello google.com, I am asdf@asdf.com", { - * urls: true, - * email: true - * } ); - * - * console.log( matches.length ); // 2 - * console.log( matches[ 0 ].getType() ); // 'url' - * console.log( matches[ 0 ].getUrl() ); // 'google.com' - * console.log( matches[ 1 ].getType() ); // 'email' - * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {Autolinker.match.Match[]} The array of Matches found in the - * given input `textOrHtml`. - */ - static parse(textOrHtml: string, options: AutolinkerConfig): Match[]; - /** - * The Autolinker version number exposed on the instance itself. - * - * Ex: 0.25.1 - */ - readonly version = "3.14.1"; - /** - * @cfg {Boolean/Object} [urls] - * - * `true` if URLs should be automatically linked, `false` if they should not - * be. Defaults to `true`. - * - * Examples: - * - * urls: true - * - * // or - * - * urls: { - * schemeMatches : true, - * wwwMatches : true, - * tldMatches : true - * } - * - * As shown above, this option also accepts an Object form with 3 properties - * to allow for more customization of what exactly gets linked. All default - * to `true`: - * - * @cfg {Boolean} [urls.schemeMatches] `true` to match URLs found prefixed - * with a scheme, i.e. `http://google.com`, or `other+scheme://google.com`, - * `false` to prevent these types of matches. - * @cfg {Boolean} [urls.wwwMatches] `true` to match urls found prefixed with - * `'www.'`, i.e. `www.google.com`. `false` to prevent these types of - * matches. Note that if the URL had a prefixed scheme, and - * `schemeMatches` is true, it will still be linked. - * @cfg {Boolean} [urls.tldMatches] `true` to match URLs with known top - * level domains (.com, .net, etc.) that are not prefixed with a scheme or - * `'www.'`. This option attempts to match anything that looks like a URL - * in the given text. Ex: `google.com`, `asdf.org/?page=1`, etc. `false` - * to prevent these types of matches. - */ - private readonly urls; - /** - * @cfg {Boolean} [email=true] - * - * `true` if email addresses should be automatically linked, `false` if they - * should not be. - */ - private readonly email; - /** - * @cfg {Boolean} [phone=true] - * - * `true` if Phone numbers ("(555)555-5555") should be automatically linked, - * `false` if they should not be. - */ - private readonly phone; - /** - * @cfg {Boolean/String} [hashtag=false] - * - * A string for the service name to have hashtags (ex: "#myHashtag") - * auto-linked to. The currently-supported values are: - * - * - 'twitter' - * - 'facebook' - * - 'instagram' - * - * Pass `false` to skip auto-linking of hashtags. - */ - private readonly hashtag; - /** - * @cfg {String/Boolean} [mention=false] - * - * A string for the service name to have mentions (ex: "@myuser") - * auto-linked to. The currently supported values are: - * - * - 'twitter' - * - 'instagram' - * - 'soundcloud' - * - * Defaults to `false` to skip auto-linking of mentions. - */ - private readonly mention; - /** - * @cfg {Boolean} [newWindow=true] - * - * `true` if the links should open in a new window, `false` otherwise. - */ - private readonly newWindow; - /** - * @cfg {Boolean/Object} [stripPrefix=true] - * - * `true` if 'http://' (or 'https://') and/or the 'www.' should be stripped - * from the beginning of URL links' text, `false` otherwise. Defaults to - * `true`. - * - * Examples: - * - * stripPrefix: true - * - * // or - * - * stripPrefix: { - * scheme : true, - * www : true - * } - * - * As shown above, this option also accepts an Object form with 2 properties - * to allow for more customization of what exactly is prevented from being - * displayed. Both default to `true`: - * - * @cfg {Boolean} [stripPrefix.scheme] `true` to prevent the scheme part of - * a URL match from being displayed to the user. Example: - * `'http://google.com'` will be displayed as `'google.com'`. `false` to - * not strip the scheme. NOTE: Only an `'http://'` or `'https://'` scheme - * will be removed, so as not to remove a potentially dangerous scheme - * (such as `'file://'` or `'javascript:'`) - * @cfg {Boolean} [stripPrefix.www] www (Boolean): `true` to prevent the - * `'www.'` part of a URL match from being displayed to the user. Ex: - * `'www.google.com'` will be displayed as `'google.com'`. `false` to not - * strip the `'www'`. - */ - private readonly stripPrefix; - /** - * @cfg {Boolean} [stripTrailingSlash=true] - * - * `true` to remove the trailing slash from URL matches, `false` to keep - * the trailing slash. - * - * Example when `true`: `http://google.com/` will be displayed as - * `http://google.com`. - */ - private readonly stripTrailingSlash; - /** - * @cfg {Boolean} [decodePercentEncoding=true] - * - * `true` to decode percent-encoded characters in URL matches, `false` to keep - * the percent-encoded characters. - * - * Example when `true`: `https://en.wikipedia.org/wiki/San_Jos%C3%A9` will - * be displayed as `https://en.wikipedia.org/wiki/San_José`. - */ - private readonly decodePercentEncoding; - /** - * @cfg {Number/Object} [truncate=0] - * - * ## Number Form - * - * A number for how many characters matched text should be truncated to - * inside the text of a link. If the matched text is over this number of - * characters, it will be truncated to this length by adding a two period - * ellipsis ('..') to the end of the string. - * - * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' - * truncated to 25 characters might look something like this: - * 'yahoo.com/some/long/pat..' - * - * Example Usage: - * - * truncate: 25 - * - * - * Defaults to `0` for "no truncation." - * - * - * ## Object Form - * - * An Object may also be provided with two properties: `length` (Number) and - * `location` (String). `location` may be one of the following: 'end' - * (default), 'middle', or 'smart'. - * - * Example Usage: - * - * truncate: { length: 25, location: 'middle' } - * - * @cfg {Number} [truncate.length=0] How many characters to allow before - * truncation will occur. Defaults to `0` for "no truncation." - * @cfg {"end"/"middle"/"smart"} [truncate.location="end"] - * - * - 'end' (default): will truncate up to the number of characters, and then - * add an ellipsis at the end. Ex: 'yahoo.com/some/long/pat..' - * - 'middle': will truncate and add the ellipsis in the middle. Ex: - * 'yahoo.com/s..th/to/a/file' - * - 'smart': for URLs where the algorithm attempts to strip out unnecessary - * parts first (such as the 'www.', then URL scheme, hash, etc.), - * attempting to make the URL human-readable before looking for a good - * point to insert the ellipsis if it is still too long. Ex: - * 'yahoo.com/some..to/a/file'. For more details, see - * {@link Autolinker.truncate.TruncateSmart}. - */ - private readonly truncate; - /** - * @cfg {String} className - * - * A CSS class name to add to the generated links. This class will be added - * to all links, as well as this class plus match suffixes for styling - * url/email/phone/hashtag/mention links differently. - * - * For example, if this config is provided as "myLink", then: - * - * - URL links will have the CSS classes: "myLink myLink-url" - * - Email links will have the CSS classes: "myLink myLink-email", and - * - Phone links will have the CSS classes: "myLink myLink-phone" - * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" - * - Mention links will have the CSS classes: "myLink myLink-mention myLink-[type]" - * where [type] is either "instagram", "twitter" or "soundcloud" - */ - private readonly className; - /** - * @cfg {Function} replaceFn - * - * A function to individually process each match found in the input string. - * - * See the class's description for usage. - * - * The `replaceFn` can be called with a different context object (`this` - * reference) using the {@link #context} cfg. - * - * This function is called with the following parameter: - * - * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which - * can be used to retrieve information about the match that the `replaceFn` - * is currently processing. See {@link Autolinker.match.Match} subclasses - * for details. - */ - private readonly replaceFn; - /** - * @cfg {Object} context - * - * The context object (`this` reference) to call the `replaceFn` with. - * - * Defaults to this Autolinker instance. - */ - private readonly context; - /** - * @cfg {Boolean} [sanitizeHtml=false] - * - * `true` to HTML-encode the start and end brackets of existing HTML tags found - * in the input string. This will escape `<` and `>` characters to `<` and - * `>`, respectively. - * - * Setting this to `true` will prevent XSS (Cross-site Scripting) attacks, - * but will remove the significance of existing HTML tags in the input string. If - * you would like to maintain the significance of existing HTML tags while also - * making the output HTML string safe, leave this option as `false` and use a - * tool like https://github.com/cure53/DOMPurify (or others) on the input string - * before running Autolinker. - */ - private readonly sanitizeHtml; - /** - * @private - * @property {Autolinker.matcher.Matcher[]} matchers - * - * The {@link Autolinker.matcher.Matcher} instances for this Autolinker - * instance. - * - * This is lazily created in {@link #getMatchers}. - */ - private matchers; - /** - * @private - * @property {Autolinker.AnchorTagBuilder} tagBuilder - * - * The AnchorTagBuilder instance used to build match replacement anchor tags. - * Note: this is lazily instantiated in the {@link #getTagBuilder} method. - */ - private tagBuilder; - /** - * @method constructor - * @param {Object} [cfg] The configuration options for the Autolinker instance, - * specified in an Object (map). - */ - constructor(cfg?: AutolinkerConfig); - /** - * Normalizes the {@link #urls} config into an Object with 3 properties: - * `schemeMatches`, `wwwMatches`, and `tldMatches`, all Booleans. - * - * See {@link #urls} config for details. - * - * @private - * @param {Boolean/Object} urls - * @return {Object} - */ - private normalizeUrlsCfg; - /** - * Normalizes the {@link #stripPrefix} config into an Object with 2 - * properties: `scheme`, and `www` - both Booleans. - * - * See {@link #stripPrefix} config for details. - * - * @private - * @param {Boolean/Object} stripPrefix - * @return {Object} - */ - private normalizeStripPrefixCfg; - /** - * Normalizes the {@link #truncate} config into an Object with 2 properties: - * `length` (Number), and `location` (String). - * - * See {@link #truncate} config for details. - * - * @private - * @param {Number/Object} truncate - * @return {Object} - */ - private normalizeTruncateCfg; - /** - * Parses the input `textOrHtml` looking for URLs, email addresses, phone - * numbers, username handles, and hashtags (depending on the configuration - * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} - * objects describing those matches (without making any replacements). - * - * This method is used by the {@link #link} method, but can also be used to - * simply do parsing of the input in order to discover what kinds of links - * there are and how many. - * - * Example usage: - * - * var autolinker = new Autolinker( { - * urls: true, - * email: true - * } ); - * - * var matches = autolinker.parse( "Hello google.com, I am asdf@asdf.com" ); - * - * console.log( matches.length ); // 2 - * console.log( matches[ 0 ].getType() ); // 'url' - * console.log( matches[ 0 ].getUrl() ); // 'google.com' - * console.log( matches[ 1 ].getType() ); // 'email' - * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' - * - * @param {String} textOrHtml The HTML or text to find matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @return {Autolinker.match.Match[]} The array of Matches found in the - * given input `textOrHtml`. - */ - parse(textOrHtml: string): Match[]; - /** - * After we have found all matches, we need to remove matches that overlap - * with a previous match. This can happen for instance with URLs, where the - * url 'google.com/#link' would match '#link' as a hashtag. Because the - * '#link' part is contained in a larger match that comes before the HashTag - * match, we'll remove the HashTag match. - * - * @private - * @param {Autolinker.match.Match[]} matches - * @return {Autolinker.match.Match[]} - */ - private compactMatches; - /** - * Removes matches for matchers that were turned off in the options. For - * example, if {@link #hashtag hashtags} were not to be matched, we'll - * remove them from the `matches` array here. - * - * Note: we *must* use all Matchers on the input string, and then filter - * them out later. For example, if the options were `{ url: false, hashtag: true }`, - * we wouldn't want to match the text '#link' as a HashTag inside of the text - * 'google.com/#link'. The way the algorithm works is that we match the full - * URL first (which prevents the accidental HashTag match), and then we'll - * simply throw away the URL match. - * - * @private - * @param {Autolinker.match.Match[]} matches The array of matches to remove - * the unwanted matches from. Note: this array is mutated for the - * removals. - * @return {Autolinker.match.Match[]} The mutated input `matches` array. - */ - private removeUnwantedMatches; - /** - * Parses the input `text` looking for URLs, email addresses, phone - * numbers, username handles, and hashtags (depending on the configuration - * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} - * objects describing those matches. - * - * This method processes a **non-HTML string**, and is used to parse and - * match within the text nodes of an HTML string. This method is used - * internally by {@link #parse}. - * - * @private - * @param {String} text The text to find matches within (depending on if the - * {@link #urls}, {@link #email}, {@link #phone}, - * {@link #hashtag}, and {@link #mention} options are enabled). This must be a non-HTML string. - * @param {Number} [offset=0] The offset of the text node within the - * original string. This is used when parsing with the {@link #parse} - * method to generate correct offsets within the {@link Autolinker.match.Match} - * instances, but may be omitted if calling this method publicly. - * @return {Autolinker.match.Match[]} The array of Matches found in the - * given input `text`. - */ - private parseText; - /** - * Automatically links URLs, Email addresses, Phone numbers, Hashtags, - * and Mentions (Twitter, Instagram, Soundcloud) found in the given chunk of HTML. Does not link - * URLs found within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to - * <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * This method finds the text around any HTML elements in the input - * `textOrHtml`, which will be the text that is processed. Any original HTML - * elements will be left as-is, as well as the text that is already wrapped - * in anchor (<a>) tags. - * - * @param {String} textOrHtml The HTML or text to autolink matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #hashtag}, and {@link #mention} options are enabled). - * @return {String} The HTML, with matches automatically linked. - */ - link(textOrHtml: string): string; - /** - * Creates the return string value for a given match in the input string. - * - * This method handles the {@link #replaceFn}, if one was provided. - * - * @private - * @param {Autolinker.match.Match} match The Match object that represents - * the match. - * @return {String} The string that the `match` should be replaced with. - * This is usually the anchor tag string, but may be the `matchStr` itself - * if the match is not to be replaced. - */ - private createMatchReturnVal; - /** - * Lazily instantiates and returns the {@link Autolinker.matcher.Matcher} - * instances for this Autolinker instance. - * - * @private - * @return {Autolinker.matcher.Matcher[]} - */ - private getMatchers; - /** - * Returns the {@link #tagBuilder} instance for this Autolinker instance, - * lazily instantiating it if it does not yet exist. - * - * @private - * @return {Autolinker.AnchorTagBuilder} - */ - private getTagBuilder; -} -export interface AutolinkerConfig { - urls?: UrlsConfig; - email?: boolean; - phone?: boolean; - hashtag?: HashtagConfig; - mention?: MentionConfig; - newWindow?: boolean; - stripPrefix?: StripPrefixConfig; - stripTrailingSlash?: boolean; - truncate?: TruncateConfig; - className?: string; - replaceFn?: ReplaceFn | null; - context?: any; - sanitizeHtml?: boolean; - decodePercentEncoding?: boolean; -} -export declare type UrlsConfig = boolean | UrlsConfigObj; -export interface UrlsConfigObj { - schemeMatches?: boolean; - wwwMatches?: boolean; - tldMatches?: boolean; -} -export declare type UrlMatchTypeOptions = 'scheme' | 'www' | 'tld'; -export declare type StripPrefixConfig = boolean | StripPrefixConfigObj; -export interface StripPrefixConfigObj { - scheme?: boolean; - www?: boolean; -} -export declare type TruncateConfig = number | TruncateConfigObj; -export interface TruncateConfigObj { - length?: number; - location?: "end" | "middle" | "smart"; -} -export declare type HashtagConfig = false | HashtagServices; -export declare type HashtagServices = 'twitter' | 'facebook' | 'instagram'; -export declare type MentionConfig = false | MentionServices; -export declare type MentionServices = 'mastodon' | 'twitter' | 'instagram' | 'soundcloud'; -export declare type ReplaceFn = (match: Match) => ReplaceFnReturn; -export declare type ReplaceFnReturn = boolean | string | HtmlTag | null | undefined | void; diff --git a/src/modules/autolinker/autolinker.js b/src/modules/autolinker/autolinker.js deleted file mode 100644 index 86ceb9f3..00000000 --- a/src/modules/autolinker/autolinker.js +++ /dev/null @@ -1,907 +0,0 @@ -import { defaults, remove, splitAndCapture } from "./utils"; -import { AnchorTagBuilder } from "./anchor-tag-builder"; -import { Match } from "./match/match"; -import { EmailMatch } from "./match/email-match"; -import { HashtagMatch } from "./match/hashtag-match"; -import { MentionMatch } from "./match/mention-match"; -import { PhoneMatch } from "./match/phone-match"; -import { UrlMatch } from "./match/url-match"; -import { Matcher } from "./matcher/matcher"; -import { HtmlTag } from "./html-tag"; -import { EmailMatcher } from "./matcher/email-matcher"; -import { UrlMatcher } from "./matcher/url-matcher"; -import { HashtagMatcher } from "./matcher/hashtag-matcher"; -import { PhoneMatcher } from "./matcher/phone-matcher"; -import { MentionMatcher } from "./matcher/mention-matcher"; -import { parseHtml } from './htmlParser/parse-html'; -/** - * @class Autolinker - * @extends Object - * - * Utility class used to process a given string of text, and wrap the matches in - * the appropriate anchor (<a>) tags to turn them into links. - * - * Any of the configuration options may be provided in an Object provided - * to the Autolinker constructor, which will configure how the {@link #link link()} - * method will process the links. - * - * For example: - * - * var autolinker = new Autolinker( { - * newWindow : false, - * truncate : 30 - * } ); - * - * var html = autolinker.link( "Joe went to www.yahoo.com" ); - * // produces: 'Joe went to yahoo.com' - * - * - * The {@link #static-link static link()} method may also be used to inline - * options into a single call, which may be more convenient for one-off uses. - * For example: - * - * var html = Autolinker.link( "Joe went to www.yahoo.com", { - * newWindow : false, - * truncate : 30 - * } ); - * // produces: 'Joe went to yahoo.com' - * - * - * ## Custom Replacements of Links - * - * If the configuration options do not provide enough flexibility, a {@link #replaceFn} - * may be provided to fully customize the output of Autolinker. This function is - * called once for each URL/Email/Phone#/Hashtag/Mention (Twitter, Instagram, Soundcloud) - * match that is encountered. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, Phone #s, Hashtags, and Mentions (Twitter, Instagram, Soundcloud) - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { - * var tag = match.buildTag(); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes - * tag.setAttr( 'rel', 'nofollow' ); - * tag.addClass( 'external-link' ); - * - * return tag; - * - * } else { - * return true; // let Autolinker perform its normal anchor tag replacement - * } - * - * case 'email' : - * var email = match.getEmail(); - * console.log( "email: ", email ); - * - * if( email === "my@own.address" ) { - * return false; // don't auto-link this particular email address; leave as-is - * } else { - * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) - * } - * - * case 'phone' : - * var phoneNumber = match.getPhoneNumber(); - * console.log( phoneNumber ); - * - * return '' + phoneNumber + ''; - * - * case 'hashtag' : - * var hashtag = match.getHashtag(); - * console.log( hashtag ); - * - * return '' + hashtag + ''; - * - * case 'mention' : - * var mention = match.getMention(); - * console.log( mention ); - * - * return '' + mention + ''; - * } - * } - * } ); - * - * - * The function may return the following values: - * - * - `true` (Boolean): Allow Autolinker to replace the match as it normally - * would. - * - `false` (Boolean): Do not replace the current match at all - leave as-is. - * - Any String: If a string is returned from the function, the string will be - * used directly as the replacement HTML for the match. - * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify - * an HTML tag before writing out its HTML text. - */ -var Autolinker = /** @class */ (function () { - /** - * @method constructor - * @param {Object} [cfg] The configuration options for the Autolinker instance, - * specified in an Object (map). - */ - function Autolinker(cfg) { - if (cfg === void 0) { cfg = {}; } - /** - * The Autolinker version number exposed on the instance itself. - * - * Ex: 0.25.1 - */ - this.version = Autolinker.version; - /** - * @cfg {Boolean/Object} [urls] - * - * `true` if URLs should be automatically linked, `false` if they should not - * be. Defaults to `true`. - * - * Examples: - * - * urls: true - * - * // or - * - * urls: { - * schemeMatches : true, - * wwwMatches : true, - * tldMatches : true - * } - * - * As shown above, this option also accepts an Object form with 3 properties - * to allow for more customization of what exactly gets linked. All default - * to `true`: - * - * @cfg {Boolean} [urls.schemeMatches] `true` to match URLs found prefixed - * with a scheme, i.e. `http://google.com`, or `other+scheme://google.com`, - * `false` to prevent these types of matches. - * @cfg {Boolean} [urls.wwwMatches] `true` to match urls found prefixed with - * `'www.'`, i.e. `www.google.com`. `false` to prevent these types of - * matches. Note that if the URL had a prefixed scheme, and - * `schemeMatches` is true, it will still be linked. - * @cfg {Boolean} [urls.tldMatches] `true` to match URLs with known top - * level domains (.com, .net, etc.) that are not prefixed with a scheme or - * `'www.'`. This option attempts to match anything that looks like a URL - * in the given text. Ex: `google.com`, `asdf.org/?page=1`, etc. `false` - * to prevent these types of matches. - */ - this.urls = {}; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [email=true] - * - * `true` if email addresses should be automatically linked, `false` if they - * should not be. - */ - this.email = true; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [phone=true] - * - * `true` if Phone numbers ("(555)555-5555") should be automatically linked, - * `false` if they should not be. - */ - this.phone = true; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean/String} [hashtag=false] - * - * A string for the service name to have hashtags (ex: "#myHashtag") - * auto-linked to. The currently-supported values are: - * - * - 'twitter' - * - 'facebook' - * - 'instagram' - * - * Pass `false` to skip auto-linking of hashtags. - */ - this.hashtag = false; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {String/Boolean} [mention=false] - * - * A string for the service name to have mentions (ex: "@myuser") - * auto-linked to. The currently supported values are: - * - * - 'twitter' - * - 'instagram' - * - 'soundcloud' - * - * Defaults to `false` to skip auto-linking of mentions. - */ - this.mention = false; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [newWindow=true] - * - * `true` if the links should open in a new window, `false` otherwise. - */ - this.newWindow = true; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean/Object} [stripPrefix=true] - * - * `true` if 'http://' (or 'https://') and/or the 'www.' should be stripped - * from the beginning of URL links' text, `false` otherwise. Defaults to - * `true`. - * - * Examples: - * - * stripPrefix: true - * - * // or - * - * stripPrefix: { - * scheme : true, - * www : true - * } - * - * As shown above, this option also accepts an Object form with 2 properties - * to allow for more customization of what exactly is prevented from being - * displayed. Both default to `true`: - * - * @cfg {Boolean} [stripPrefix.scheme] `true` to prevent the scheme part of - * a URL match from being displayed to the user. Example: - * `'http://google.com'` will be displayed as `'google.com'`. `false` to - * not strip the scheme. NOTE: Only an `'http://'` or `'https://'` scheme - * will be removed, so as not to remove a potentially dangerous scheme - * (such as `'file://'` or `'javascript:'`) - * @cfg {Boolean} [stripPrefix.www] www (Boolean): `true` to prevent the - * `'www.'` part of a URL match from being displayed to the user. Ex: - * `'www.google.com'` will be displayed as `'google.com'`. `false` to not - * strip the `'www'`. - */ - this.stripPrefix = { scheme: true, www: true }; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [stripTrailingSlash=true] - * - * `true` to remove the trailing slash from URL matches, `false` to keep - * the trailing slash. - * - * Example when `true`: `http://google.com/` will be displayed as - * `http://google.com`. - */ - this.stripTrailingSlash = true; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [decodePercentEncoding=true] - * - * `true` to decode percent-encoded characters in URL matches, `false` to keep - * the percent-encoded characters. - * - * Example when `true`: `https://en.wikipedia.org/wiki/San_Jos%C3%A9` will - * be displayed as `https://en.wikipedia.org/wiki/San_José`. - */ - this.decodePercentEncoding = true; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Number/Object} [truncate=0] - * - * ## Number Form - * - * A number for how many characters matched text should be truncated to - * inside the text of a link. If the matched text is over this number of - * characters, it will be truncated to this length by adding a two period - * ellipsis ('..') to the end of the string. - * - * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' - * truncated to 25 characters might look something like this: - * 'yahoo.com/some/long/pat..' - * - * Example Usage: - * - * truncate: 25 - * - * - * Defaults to `0` for "no truncation." - * - * - * ## Object Form - * - * An Object may also be provided with two properties: `length` (Number) and - * `location` (String). `location` may be one of the following: 'end' - * (default), 'middle', or 'smart'. - * - * Example Usage: - * - * truncate: { length: 25, location: 'middle' } - * - * @cfg {Number} [truncate.length=0] How many characters to allow before - * truncation will occur. Defaults to `0` for "no truncation." - * @cfg {"end"/"middle"/"smart"} [truncate.location="end"] - * - * - 'end' (default): will truncate up to the number of characters, and then - * add an ellipsis at the end. Ex: 'yahoo.com/some/long/pat..' - * - 'middle': will truncate and add the ellipsis in the middle. Ex: - * 'yahoo.com/s..th/to/a/file' - * - 'smart': for URLs where the algorithm attempts to strip out unnecessary - * parts first (such as the 'www.', then URL scheme, hash, etc.), - * attempting to make the URL human-readable before looking for a good - * point to insert the ellipsis if it is still too long. Ex: - * 'yahoo.com/some..to/a/file'. For more details, see - * {@link Autolinker.truncate.TruncateSmart}. - */ - this.truncate = { length: 0, location: 'end' }; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {String} className - * - * A CSS class name to add to the generated links. This class will be added - * to all links, as well as this class plus match suffixes for styling - * url/email/phone/hashtag/mention links differently. - * - * For example, if this config is provided as "myLink", then: - * - * - URL links will have the CSS classes: "myLink myLink-url" - * - Email links will have the CSS classes: "myLink myLink-email", and - * - Phone links will have the CSS classes: "myLink myLink-phone" - * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" - * - Mention links will have the CSS classes: "myLink myLink-mention myLink-[type]" - * where [type] is either "instagram", "twitter" or "soundcloud" - */ - this.className = ''; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Function} replaceFn - * - * A function to individually process each match found in the input string. - * - * See the class's description for usage. - * - * The `replaceFn` can be called with a different context object (`this` - * reference) using the {@link #context} cfg. - * - * This function is called with the following parameter: - * - * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which - * can be used to retrieve information about the match that the `replaceFn` - * is currently processing. See {@link Autolinker.match.Match} subclasses - * for details. - */ - this.replaceFn = null; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Object} context - * - * The context object (`this` reference) to call the `replaceFn` with. - * - * Defaults to this Autolinker instance. - */ - this.context = undefined; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @cfg {Boolean} [sanitizeHtml=false] - * - * `true` to HTML-encode the start and end brackets of existing HTML tags found - * in the input string. This will escape `<` and `>` characters to `<` and - * `>`, respectively. - * - * Setting this to `true` will prevent XSS (Cross-site Scripting) attacks, - * but will remove the significance of existing HTML tags in the input string. If - * you would like to maintain the significance of existing HTML tags while also - * making the output HTML string safe, leave this option as `false` and use a - * tool like https://github.com/cure53/DOMPurify (or others) on the input string - * before running Autolinker. - */ - this.sanitizeHtml = false; // default value just to get the above doc comment in the ES5 output and documentation generator - /** - * @private - * @property {Autolinker.matcher.Matcher[]} matchers - * - * The {@link Autolinker.matcher.Matcher} instances for this Autolinker - * instance. - * - * This is lazily created in {@link #getMatchers}. - */ - this.matchers = null; - /** - * @private - * @property {Autolinker.AnchorTagBuilder} tagBuilder - * - * The AnchorTagBuilder instance used to build match replacement anchor tags. - * Note: this is lazily instantiated in the {@link #getTagBuilder} method. - */ - this.tagBuilder = null; - // Note: when `this.something` is used in the rhs of these assignments, - // it refers to the default values set above the constructor - this.urls = this.normalizeUrlsCfg(cfg.urls); - this.email = typeof cfg.email === 'boolean' ? cfg.email : this.email; - this.phone = typeof cfg.phone === 'boolean' ? cfg.phone : this.phone; - this.hashtag = cfg.hashtag || this.hashtag; - this.mention = cfg.mention || this.mention; - this.newWindow = typeof cfg.newWindow === 'boolean' ? cfg.newWindow : this.newWindow; - this.stripPrefix = this.normalizeStripPrefixCfg(cfg.stripPrefix); - this.stripTrailingSlash = typeof cfg.stripTrailingSlash === 'boolean' ? cfg.stripTrailingSlash : this.stripTrailingSlash; - this.decodePercentEncoding = typeof cfg.decodePercentEncoding === 'boolean' ? cfg.decodePercentEncoding : this.decodePercentEncoding; - this.sanitizeHtml = cfg.sanitizeHtml || false; - // Validate the value of the `mention` cfg - var mention = this.mention; - if (mention !== false && mention !== 'mastodon' && mention !== 'twitter' && mention !== 'instagram' && mention !== 'soundcloud') { - throw new Error("invalid `mention` cfg - see docs"); - } - // Validate the value of the `hashtag` cfg - var hashtag = this.hashtag; - if (hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' && hashtag !== 'instagram') { - throw new Error("invalid `hashtag` cfg - see docs"); - } - this.truncate = this.normalizeTruncateCfg(cfg.truncate); - this.className = cfg.className || this.className; - this.replaceFn = cfg.replaceFn || this.replaceFn; - this.context = cfg.context || this; - } - /** - * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, - * Hashtags, and Mentions found in the given chunk of HTML. Does not link URLs - * found within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * Example: - * - * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); - * // Produces: "Go to google.com" - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within (depending - * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #mention}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {String} The HTML text, with matches automatically linked. - */ - Autolinker.link = function (textOrHtml, options) { - var autolinker = new Autolinker(options); - return autolinker.link(textOrHtml); - }; - /** - * Parses the input `textOrHtml` looking for URLs, email addresses, phone - * numbers, username handles, and hashtags (depending on the configuration - * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} - * objects describing those matches (without making any replacements). - * - * Note that if parsing multiple pieces of text, it is slightly more efficient - * to create an Autolinker instance, and use the instance-level {@link #parse} - * method. - * - * Example: - * - * var matches = Autolinker.parse( "Hello google.com, I am asdf@asdf.com", { - * urls: true, - * email: true - * } ); - * - * console.log( matches.length ); // 2 - * console.log( matches[ 0 ].getType() ); // 'url' - * console.log( matches[ 0 ].getUrl() ); // 'google.com' - * console.log( matches[ 1 ].getType() ); // 'email' - * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {Autolinker.match.Match[]} The array of Matches found in the - * given input `textOrHtml`. - */ - Autolinker.parse = function (textOrHtml, options) { - var autolinker = new Autolinker(options); - return autolinker.parse(textOrHtml); - }; - /** - * Normalizes the {@link #urls} config into an Object with 3 properties: - * `schemeMatches`, `wwwMatches`, and `tldMatches`, all Booleans. - * - * See {@link #urls} config for details. - * - * @private - * @param {Boolean/Object} urls - * @return {Object} - */ - Autolinker.prototype.normalizeUrlsCfg = function (urls) { - if (urls == null) - urls = true; // default to `true` - if (typeof urls === 'boolean') { - return { schemeMatches: urls, wwwMatches: urls, tldMatches: urls }; - } - else { // object form - return { - schemeMatches: typeof urls.schemeMatches === 'boolean' ? urls.schemeMatches : true, - wwwMatches: typeof urls.wwwMatches === 'boolean' ? urls.wwwMatches : true, - tldMatches: typeof urls.tldMatches === 'boolean' ? urls.tldMatches : true - }; - } - }; - /** - * Normalizes the {@link #stripPrefix} config into an Object with 2 - * properties: `scheme`, and `www` - both Booleans. - * - * See {@link #stripPrefix} config for details. - * - * @private - * @param {Boolean/Object} stripPrefix - * @return {Object} - */ - Autolinker.prototype.normalizeStripPrefixCfg = function (stripPrefix) { - if (stripPrefix == null) - stripPrefix = true; // default to `true` - if (typeof stripPrefix === 'boolean') { - return { scheme: stripPrefix, www: stripPrefix }; - } - else { // object form - return { - scheme: typeof stripPrefix.scheme === 'boolean' ? stripPrefix.scheme : true, - www: typeof stripPrefix.www === 'boolean' ? stripPrefix.www : true - }; - } - }; - /** - * Normalizes the {@link #truncate} config into an Object with 2 properties: - * `length` (Number), and `location` (String). - * - * See {@link #truncate} config for details. - * - * @private - * @param {Number/Object} truncate - * @return {Object} - */ - Autolinker.prototype.normalizeTruncateCfg = function (truncate) { - if (typeof truncate === 'number') { - return { length: truncate, location: 'end' }; - } - else { // object, or undefined/null - return defaults(truncate || {}, { - length: Number.POSITIVE_INFINITY, - location: 'end' - }); - } - }; - /** - * Parses the input `textOrHtml` looking for URLs, email addresses, phone - * numbers, username handles, and hashtags (depending on the configuration - * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} - * objects describing those matches (without making any replacements). - * - * This method is used by the {@link #link} method, but can also be used to - * simply do parsing of the input in order to discover what kinds of links - * there are and how many. - * - * Example usage: - * - * var autolinker = new Autolinker( { - * urls: true, - * email: true - * } ); - * - * var matches = autolinker.parse( "Hello google.com, I am asdf@asdf.com" ); - * - * console.log( matches.length ); // 2 - * console.log( matches[ 0 ].getType() ); // 'url' - * console.log( matches[ 0 ].getUrl() ); // 'google.com' - * console.log( matches[ 1 ].getType() ); // 'email' - * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' - * - * @param {String} textOrHtml The HTML or text to find matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #hashtag}, and {@link #mention} options are enabled). - * @return {Autolinker.match.Match[]} The array of Matches found in the - * given input `textOrHtml`. - */ - Autolinker.prototype.parse = function (textOrHtml) { - var _this = this; - var skipTagNames = ['a', 'style', 'script'], skipTagsStackCount = 0, // used to only Autolink text outside of anchor/script/style tags. We don't want to autolink something that is already linked inside of an tag, for instance - matches = []; - // Find all matches within the `textOrHtml` (but not matches that are - // already nested within ,