Files
kotlin-fork/libraries/stdlib/js/src/kotlin/regex.kt
T
2019-02-18 21:05:46 +03:00

243 lines
11 KiB
Kotlin

/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.text
import kotlin.js.RegExp
/**
* Provides enumeration values to use to set regular expression options.
*/
public actual enum class RegexOption(val value: String) {
/** Enables case-insensitive matching. */
IGNORE_CASE("i"),
/** Enables multiline mode.
*
* In multiline mode the expressions `^` and `$` match just after or just before,
* respectively, a line terminator or the end of the input sequence. */
MULTILINE("m")
}
/**
* Represents the results from a single capturing group within a [MatchResult] of [Regex].
*
* @param value The value of captured group.
*/
public actual data class MatchGroup(actual val value: String)
/**
* Represents a compiled regular expression.
* Provides functions to match strings in text with a pattern, replace the found occurrences and split text around matches.
*
* For pattern syntax reference see [MDN RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Special_characters_meaning_in_regular_expressions)
* and [http://www.w3schools.com/jsref/jsref_obj_regexp.asp](https://www.w3schools.com/jsref/jsref_obj_regexp.asp).
*
* @constructor Creates a regular expression from the specified [pattern] string and the specified set of [options].
*/
public actual class Regex actual constructor(pattern: String, options: Set<RegexOption>) {
/** Creates a regular expression from the specified [pattern] string and the specified single [option]. */
public actual constructor(pattern: String, option: RegexOption) : this(pattern, setOf(option))
/** Creates a regular expression from the specified [pattern] string and the default options. */
public actual constructor(pattern: String) : this(pattern, emptySet())
/** The pattern string of this regular expression. */
public actual val pattern: String = pattern
/** The set of options that were used to create this regular expression. */
public actual val options: Set<RegexOption> = options.toSet()
private val nativePattern: RegExp = RegExp(pattern, options.map { it.value }.joinToString(separator = "") + "g")
/** Indicates whether the regular expression matches the entire [input]. */
public actual infix fun matches(input: CharSequence): Boolean {
nativePattern.reset()
val match = nativePattern.exec(input.toString())
return match != null && match.index == 0 && nativePattern.lastIndex == input.length
}
/** Indicates whether the regular expression can find at least one match in the specified [input]. */
public actual fun containsMatchIn(input: CharSequence): Boolean {
nativePattern.reset()
return nativePattern.test(input.toString())
}
/** Returns the first match of a regular expression in the [input], beginning at the specified [startIndex].
*
* @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()`
* @return An instance of [MatchResult] if match was found or `null` otherwise.
*/
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun find(input: CharSequence, startIndex: Int = 0): MatchResult? = nativePattern.findNext(input.toString(), startIndex)
/**
* Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex].
*
* @sample samples.text.Regexps.findAll
*/
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun findAll(input: CharSequence, startIndex: Int = 0): Sequence<MatchResult> =
generateSequence({ find(input, startIndex) }, { match -> match.next() })
/**
* Attempts to match the entire [input] CharSequence against the pattern.
*
* @return An instance of [MatchResult] if the entire input matches or `null` otherwise.
*/
public actual fun matchEntire(input: CharSequence): MatchResult? {
if (pattern.startsWith('^') && pattern.endsWith('$'))
return find(input)
else
return Regex("^${pattern.trimStart('^').trimEnd('$')}$", options).find(input)
}
/**
* Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression.
*
* @param replacement A replacement expression that can include substitutions. See [String.prototype.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function docs for details.
*/
public actual fun replace(input: CharSequence, replacement: String): String = input.toString().nativeReplace(nativePattern, replacement)
/**
* Replaces all occurrences of this regular expression in the specified [input] string with the result of
* the given function [transform] that takes [MatchResult] and returns a string to be used as a
* replacement for that match.
*/
public actual inline fun replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String {
var match = find(input)
if (match == null) return input.toString()
var lastStart = 0
val length = input.length
val sb = StringBuilder(length)
do {
val foundMatch = match!!
sb.append(input, lastStart, foundMatch.range.start)
sb.append(transform(foundMatch))
lastStart = foundMatch.range.endInclusive + 1
match = foundMatch.next()
} while (lastStart < length && match != null)
if (lastStart < length) {
sb.append(input, lastStart, length)
}
return sb.toString()
}
/**
* Replaces the first occurrence of this regular expression in the specified [input] string with specified [replacement] expression.
*
* @param replacement A replacement expression that can include substitutions. See [String.prototype.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function docs for details.
*/
public actual fun replaceFirst(input: CharSequence, replacement: String): String {
val nonGlobalOptions = options.map { it.value }.joinToString(separator = "")
return input.toString().nativeReplace(RegExp(pattern, nonGlobalOptions), replacement)
}
/**
* Splits the [input] CharSequence around matches of this regular expression.
*
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
* Zero by default means no limit is set.
*/
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun split(input: CharSequence, limit: Int = 0): List<String> {
require(limit >= 0) { "Limit must be non-negative, but was $limit" }
val matches = findAll(input).let { if (limit == 0) it else it.take(limit - 1) }
val result = mutableListOf<String>()
var lastStart = 0
for (match in matches) {
result.add(input.subSequence(lastStart, match.range.start).toString())
lastStart = match.range.endInclusive + 1
}
result.add(input.subSequence(lastStart, input.length).toString())
return result
}
/**
* Returns the string representation of this regular expression, namely the [pattern] of this regular expression.
*
* Note that another regular expression constructed from the same pattern string may have different [options]
* and may match strings differently.
*/
public override fun toString(): String = nativePattern.toString()
actual companion object {
/**
* Returns a regular expression that matches the specified [literal] string literally.
* No characters of that string will have special meaning when searching for an occurrence of the regular expression.
*/
public actual fun fromLiteral(literal: String): Regex = Regex(escape(literal))
/**
* Returns a regular expression pattern string that matches the specified [literal] string literally.
* No characters of that string will have special meaning when searching for an occurrence of the regular expression.
*/
public actual fun escape(literal: String): String = literal.nativeReplace(patternEscape, "\\$&")
/**
* Returns a literal replacement expression for the specified [literal] string.
* No characters of that string will have special meaning when it is used as a replacement string in [Regex.replace] function.
*/
public actual fun escapeReplacement(literal: String): String = literal.nativeReplace(replacementEscape, "$$$$")
private val patternEscape = RegExp("""[-\\^$*+?.()|[\]{}]""", "g")
private val replacementEscape = RegExp("""\$""", "g")
}
}
/** Creates a regular expression from the specified [pattern] string and the specified single [option]. */
@Deprecated("Provided for binary compatibility", level = DeprecationLevel.HIDDEN)
@JsName("Regex_sb3q2\$")
public fun Regex_0(pattern: String, option: RegexOption): Regex = Regex(pattern, setOf(option))
/** Creates a regular expression from the specified [pattern] string and the default options. */
@Deprecated("Provided for binary compatibility", level = DeprecationLevel.HIDDEN)
@JsName("Regex_61zpoe\$")
public fun Regex_1(pattern: String): Regex = Regex(pattern, emptySet())
private fun RegExp.findNext(input: String, from: Int): MatchResult? {
this.lastIndex = from
val match = exec(input)
if (match == null) return null
val range = match.index..lastIndex - 1
return object : MatchResult {
override val range: IntRange = range
override val value: String
get() = match[0]!!
override val groups: MatchGroupCollection = object : MatchGroupCollection, AbstractCollection<MatchGroup?>() {
override val size: Int get() = match.length
override fun iterator(): Iterator<MatchGroup?> = indices.asSequence().map { this[it] }.iterator()
override fun get(index: Int): MatchGroup? = match[index]?.let { MatchGroup(it) }
}
private var groupValues_: List<String>? = null
override val groupValues: List<String>
get() {
if (groupValues_ == null) {
groupValues_ = object : AbstractList<String>() {
override val size: Int get() = match.length
override fun get(index: Int): String = match[index] ?: ""
}
}
return groupValues_!!
}
override fun next(): MatchResult? = this@findNext.findNext(input, if (range.isEmpty()) range.start + 1 else range.endInclusive + 1)
}
}