fix: Use deprecated members to ensure backwards compatibility

By migrating to early to new APIs of ReVanced Patcher, if you were to use old versions of ReVanced Patcher, you would get compatibility issues. By using deprecated members until most have updated ReVanced Patcher, we can ensure seamless migration.
This commit is contained in:
oSumAtrIX 2024-02-22 00:05:41 +01:00
parent 930dc297c1
commit 083bd40092
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
24 changed files with 174 additions and 108 deletions

View File

@ -1746,7 +1746,6 @@ public final class app/revanced/util/ResourceUtilsKt {
public static final fun asSequence (Lorg/w3c/dom/NodeList;)Lkotlin/sequences/Sequence;
public static final fun childElementsSequence (Lorg/w3c/dom/Node;)Lkotlin/sequences/Sequence;
public static final fun copyResources (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;)V
public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/Document;Lapp/revanced/patcher/util/Document;)Ljava/lang/AutoCloseable;
public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/DomFileEditor;Lapp/revanced/patcher/util/DomFileEditor;)Ljava/lang/AutoCloseable;
public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V
public static final fun forEachChildElement (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V

View File

@ -14,7 +14,9 @@ object ExportAllActivitiesPatch : ResourcePatch() {
private const val EXPORTED_FLAG = "android:exported"
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val activities = document.getElementsByTagName("activity")
for (i in 0..activities.length) {

View File

@ -14,7 +14,9 @@ object PredictiveBackGesturePatch : ResourcePatch() {
private const val FLAG = "android:enableOnBackInvokedCallback"
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
with(document.getElementsByTagName("application").item(0)) {
if (attributes.getNamedItem(FLAG) != null) return@with

View File

@ -13,7 +13,9 @@ import org.w3c.dom.Element
@Suppress("unused")
object EnableAndroidDebuggingPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val applicationNode =
document
.getElementsByTagName("application")

View File

@ -16,10 +16,12 @@ import java.io.File
@Suppress("unused")
object OverrideCertificatePinningPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
val resXmlDirectory = context.get("res/xml", false)
val resXmlDirectory = context.get("res/xml")
// Add android:networkSecurityConfig="@xml/network_security_config" and the "networkSecurityConfig" attribute if it does not exist.
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val applicationNode = document.getElementsByTagName("application").item(0) as Element
if (!applicationNode.hasAttribute("networkSecurityConfig")) {

View File

@ -52,7 +52,9 @@ object ChangePackageNamePatch : ResourcePatch(), Closeable {
}
override fun close() =
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val replacementPackageName = packageNameOption.value
val manifest = document.getElementsByTagName("manifest").item(0) as Element

View File

@ -5,7 +5,7 @@ import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.Document
import app.revanced.patcher.util.DomFileEditor
import app.revanced.patches.all.misc.resources.AddResourcesPatch.resources
import app.revanced.util.*
import app.revanced.util.resource.ArrayResource
@ -92,8 +92,10 @@ object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseRes
// instead of overwriting it.
// This covers the example case such as adding strings and arrays of the same value.
getOrPut(value, ::mutableMapOf).apply {
context.document[stream].use {
it.getElementsByTagName("app").asSequence().forEach { app ->
context.xmlEditor[stream].use { editor ->
val document = editor.file
document.getElementsByTagName("app").asSequence().forEach { app ->
val appId = app.attributes.getNamedItem("id").textContent
getOrPut(appId, ::mutableMapOf).apply {
@ -237,7 +239,7 @@ object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseRes
* This is called after all patches that depend on [AddResourcesPatch] have been executed.
*/
override fun close() {
operator fun MutableMap<String, Pair<Document, Node>>.invoke(
operator fun MutableMap<String, Pair<DomFileEditor, Node>>.invoke(
value: Value,
resource: BaseResource,
) {
@ -253,16 +255,18 @@ object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseRes
getOrPut(resourceFileName) {
val targetFile =
context.get("res/$value/$resourceFileName.xml", false).also {
context.get("res/$value/$resourceFileName.xml").also {
it.parentFile?.mkdirs()
it.createNewFile()
}
context.document[targetFile.path].let { document ->
context.xmlEditor[targetFile.path].let { editor ->
val document = editor.file
// Save the target node here as well
// in order to avoid having to call document.getNode("resources")
// but also save the document so that it can be closed later.
document to document.getNode("resources")
editor to document.getNode("resources")
}
}.let { (_, targetNode) ->
targetNode.addResource(resource) { invoke(value, it) }
@ -276,7 +280,7 @@ object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseRes
// This is done to prevent having to open the files for every resource that is added.
// Instead, it is cached once and reused for resources of the same value.
// This map is later accessed to close all documents for the current resource value.
val documents = mutableMapOf<String, Pair<Document, Node>>()
val documents = mutableMapOf<String, Pair<DomFileEditor, Node>>()
resources.forEach { resource -> documents(value, resource) }

View File

@ -8,7 +8,9 @@ import org.w3c.dom.Element
@Patch(description = "Sets allowAudioPlaybackCapture in manifest to true.")
internal object RemoveCaptureRestrictionResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
// get the application node
val applicationNode =
document

View File

@ -15,7 +15,9 @@ import org.w3c.dom.Element
@Suppress("unused")
object RemoveBroadcastsRestrictionPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val applicationNode =
document
.getElementsByTagName("application")

View File

@ -9,8 +9,10 @@ object HideBannerPatch : ResourcePatch() {
private const val RESOURCE_FILE_PATH = "res/layout/merge_listheader_link_detail.xml"
override fun execute(context: ResourceContext) {
context.document[RESOURCE_FILE_PATH].use {
it.getElementsByTagName("merge").item(0).childNodes.apply {
context.xmlEditor[RESOURCE_FILE_PATH].use { editor ->
val document = editor.file
document.getElementsByTagName("merge").item(0).childNodes.apply {
val attributes = arrayOf("height", "width")
for (i in 1 until length) {

View File

@ -60,7 +60,9 @@ abstract class BaseGmsCoreSupportResourcePatch(
appendChild(child)
}
document["AndroidManifest.xml"].use { document ->
xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
val applicationNode =
document
.getElementsByTagName("application")
@ -92,8 +94,8 @@ abstract class BaseGmsCoreSupportResourcePatch(
private fun ResourceContext.patchManifest() {
val packageName = ChangePackageNamePatch.setOrGetFallbackPackageName(toPackageName)
val manifest = this.get("AndroidManifest.xml", false).readText()
this.get("AndroidManifest.xml", false).writeText(
val manifest = this.get("AndroidManifest.xml").readText()
this.get("AndroidManifest.xml").writeText(
manifest.replace(
"package=\"$fromPackageName",
"package=\"$packageName",

View File

@ -16,14 +16,16 @@ object ResourceMappingPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
// save the file in memory to concurrently read from
val resourceXmlFile = context.get("res/values/public.xml", false).readBytes()
val resourceXmlFile = context.get("res/values/public.xml").readBytes()
// create a synchronized list to store the resource mappings
val mappings = Collections.synchronizedList(mutableListOf<ResourceElement>())
for (threadIndex in 0 until THREAD_COUNT) {
threadPoolExecutor.execute thread@{
context.document[resourceXmlFile.inputStream()].use { document ->
context.xmlEditor[resourceXmlFile.inputStream()].use { editor ->
val document = editor.file
val resources = document.documentElement.childNodes
val resourcesLength = resources.length
val jobSize = resourcesLength / THREAD_COUNT

View File

@ -51,13 +51,17 @@ abstract class BaseSettingsResourcePatch(
// Add the root preference to an existing fragment if needed.
rootPreference?.let { (intentPreference, fragment) ->
context.document["res/xml/$fragment.xml"].use {
it.getNode("PreferenceScreen").addPreference(intentPreference)
context.xmlEditor["res/xml/$fragment.xml"].use { editor ->
val document = editor.file
document.getNode("PreferenceScreen").addPreference(intentPreference)
}
}
// Add all preferences to the ReVanced fragment.
context.document["res/xml/revanced_prefs.xml"].use { document ->
context.xmlEditor["res/xml/revanced_prefs.xml"].use { editor ->
val document = editor.file
val revancedPreferenceScreenNode = document.getNode("PreferenceScreen")
forEach { revancedPreferenceScreenNode.addPreference(it) }
}

View File

@ -54,7 +54,9 @@ object CustomThemePatch : ResourcePatch() {
val accentColor = accentColor!!
val accentColorPressed = accentColorPressed!!
context.document["res/values/colors.xml"].use { document ->
context.xmlEditor["res/values/colors.xml"].use { editor ->
val document = editor.file
val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
for (i in 0 until resourcesNode.childNodes.length) {

View File

@ -16,7 +16,7 @@ import java.nio.file.Files
@Suppress("unused")
object DynamicColorPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
val resDirectory = context.get("res", false)
val resDirectory = context.get("res")
if (!resDirectory.isDirectory) throw PatchException("The res folder can not be found.")
val valuesV31Directory = resDirectory.resolve("values-v31")
@ -35,7 +35,9 @@ object DynamicColorPatch : ResourcePatch() {
}
}
context.document["res/values-v31/colors.xml"].use { document ->
context.xmlEditor["res/values-v31/colors.xml"].use { editor ->
val document = editor.file
mapOf(
"ps__twitter_blue" to "@color/twitter_blue",
"ps__twitter_blue_pressed" to "@color/twitter_blue_fill_pressed",
@ -55,7 +57,9 @@ object DynamicColorPatch : ResourcePatch() {
}
}
context.document["res/values-night-v31/colors.xml"].use { document ->
context.xmlEditor["res/values-night-v31/colors.xml"].use { editor ->
val document = editor.file
mapOf(
"twitter_blue" to "@android:color/system_accent1_200",
"twitter_blue_fill_pressed" to "@android:color/system_accent1_300",

View File

@ -81,7 +81,7 @@ object CustomBrandingPatch : ResourcePatch() {
}.let { resourceGroups ->
if (icon != REVANCED_ICON) {
val path = File(icon)
val resourceDirectory = context.get("res", false)
val resourceDirectory = context.get("res")
resourceGroups.forEach { group ->
val fromDirectory = path.resolve(group.resourceDirectoryName)
@ -102,7 +102,7 @@ object CustomBrandingPatch : ResourcePatch() {
appName?.let { name ->
// Change the app name.
val manifest = context.get("AndroidManifest.xml", false)
val manifest = context.get("AndroidManifest.xml")
manifest.writeText(
manifest.readText()
.replace(

View File

@ -71,7 +71,7 @@ object ChangeHeaderPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
// The directories to copy the header to.
val targetResourceDirectories = targetResourceDirectoryNames.keys.mapNotNull {
context.get("res", false).resolve(it).takeIf(File::exists)
context.get("res").resolve(it).takeIf(File::exists)
}
// The files to replace in the target directories.
val targetResourceFiles = targetResourceDirectoryNames.keys.map { directoryName ->
@ -120,7 +120,7 @@ object ChangeHeaderPatch : ResourcePatch() {
// For each source folder, copy the files to the target resource directories.
sourceFolders.forEach { dpiSourceFolder ->
val targetDpiFolder = context.get("res", false).resolve(dpiSourceFolder.name)
val targetDpiFolder = context.get("res").resolve(dpiSourceFolder.name)
if (!targetDpiFolder.exists()) return@forEach
val imgSourceFiles = dpiSourceFolder.listFiles { file -> file.isFile }!!

View File

@ -37,7 +37,9 @@ object PlayerControlsBackgroundPatch : ResourcePatch() {
private const val RESOURCE_FILE_PATH = "res/drawable/player_button_circle_background.xml"
override fun execute(context: ResourceContext) {
context.document[RESOURCE_FILE_PATH].use { document ->
context.xmlEditor[RESOURCE_FILE_PATH].use { editor ->
val document = editor.file
document.doRecursively node@{ node ->
if (node !is Element) return@node

View File

@ -29,7 +29,9 @@ internal object SeekbarColorResourcePatch : ResourcePatch() {
findColorResource("inline_time_bar_played_not_highlighted_color")
// Edit the resume playback drawable and replace the progress bar with a custom drawable
context.document["res/drawable/resume_playback_progressbar_drawable.xml"].use { document ->
context.xmlEditor["res/drawable/resume_playback_progressbar_drawable.xml"].use { editor ->
val document = editor.file
val layerList = document.getElementsByTagName("layer-list").item(0) as Element
val progressNode = layerList.getElementsByTagName("item").item(1) as Element
if (!progressNode.getAttributeNode("android:id").value.endsWith("progress")) {

View File

@ -65,12 +65,14 @@ internal object SponsorBlockResourcePatch : ResourcePatch() {
)!!
var modifiedControlsLayout = false
val targetDocument = context.document["res/layout/youtube_controls_layout.xml"]
val editor = context.xmlEditor["res/layout/youtube_controls_layout.xml"]
"RelativeLayout".copyXmlNode(
context.document[hostingResourceStream],
targetDocument,
context.xmlEditor[hostingResourceStream],
editor,
).also {
val children = targetDocument.getElementsByTagName("RelativeLayout").item(0).childNodes
val document = editor.file
val children = document.getElementsByTagName("RelativeLayout").item(0).childNodes
// Replace the startOf with the voting button view so that the button does not overlap
for (i in 1 until children.length) {

View File

@ -35,7 +35,9 @@ internal object ThemeResourcePatch : ResourcePatch() {
)
// Edit theme colors via resources.
context.document["res/values/colors.xml"].use { document ->
context.xmlEditor["res/values/colors.xml"].use { editor ->
val document = editor.file
val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
val children = resourcesNode.childNodes
@ -76,8 +78,10 @@ internal object ThemeResourcePatch : ResourcePatch() {
)
splashScreenResourceFiles.forEach editSplashScreen@{ resourceFile ->
context.document[resourceFile].use {
val layerList = it.getElementsByTagName("layer-list").item(0) as Element
context.xmlEditor[resourceFile].use { editor ->
val document = editor.file
val layerList = document.getElementsByTagName("layer-list").item(0) as Element
val childNodes = layerList.childNodes
for (i in 0 until childNodes.length) {
@ -99,11 +103,13 @@ internal object ThemeResourcePatch : ResourcePatch() {
colorName: String,
colorValue: String,
) {
context.document[resourceFile].use {
val resourcesNode = it.getElementsByTagName("resources").item(0) as Element
context.xmlEditor[resourceFile].use { editor ->
val document = editor.file
val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
resourcesNode.appendChild(
it.createElement("color").apply {
document.createElement("color").apply {
setAttribute("name", colorName)
setAttribute("category", "color")
textContent = colorValue

View File

@ -3,7 +3,7 @@ package app.revanced.patches.youtube.misc.playercontrols
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.Document
import app.revanced.patcher.util.DomFileEditor
import app.revanced.patches.shared.misc.mapping.ResourceMappingPatch
import java.io.Closeable
@ -18,14 +18,13 @@ object BottomControlsResourcePatch : ResourcePatch(), Closeable {
private var lastLeftOf = "fullscreen_button"
private lateinit var resourceContext: ResourceContext
private lateinit var targetDocument: Document
private lateinit var targetDocumentEditor: DomFileEditor
override fun execute(context: ResourceContext) {
resourceContext = context
targetDocument = context.document[TARGET_RESOURCE]
targetDocumentEditor = context.xmlEditor[TARGET_RESOURCE]
bottomUiContainerResourceId =
ResourceMappingPatch.resourceMappings
bottomUiContainerResourceId = ResourceMappingPatch.resourceMappings
.single { it.type == "id" && it.name == "bottom_ui_container_stub" }.id
}
@ -35,21 +34,21 @@ object BottomControlsResourcePatch : ResourcePatch(), Closeable {
* @param resourceDirectoryName The name of the directory containing the hosting resource.
*/
fun addControls(resourceDirectoryName: String) {
val sourceDocument =
resourceContext.document[
val sourceDocumentEditor = resourceContext.xmlEditor[
this::class.java.classLoader.getResourceAsStream(
"$resourceDirectoryName/host/layout/$TARGET_RESOURCE_NAME",
)!!,
]
val sourceDocument = sourceDocumentEditor.file
val targetDocument = targetDocumentEditor.file
val targetElement = "android.support.constraint.ConstraintLayout"
val targetElementTag = "android.support.constraint.ConstraintLayout"
val hostElements = sourceDocument.getElementsByTagName(targetElement).item(0).childNodes
val sourceElements = sourceDocument.getElementsByTagName(targetElementTag).item(0).childNodes
val targetElement = targetDocument.getElementsByTagName(targetElementTag).item(0)
val destinationElement = targetDocument.getElementsByTagName(targetElement).item(0)
for (index in 1 until hostElements.length) {
val element = hostElements.item(index).cloneNode(true)
for (index in 1 until sourceElements.length) {
val element = sourceElements.item(index).cloneNode(true)
// If the element has no attributes there's no point to adding it to the destination.
if (!element.hasAttributes()) continue
@ -65,10 +64,10 @@ object BottomControlsResourcePatch : ResourcePatch(), Closeable {
// Add the element.
targetDocument.adoptNode(element)
destinationElement.appendChild(element)
targetElement.appendChild(element)
}
sourceDocument.close()
sourceDocumentEditor.close()
}
override fun close() = targetDocument.close()
override fun close() = targetDocumentEditor.close()
}

View File

@ -43,7 +43,9 @@ object SettingsResourcePatch : BaseSettingsResourcePatch(
// Modify the manifest and add a data intent filter to the LicenseActivity.
// Some devices freak out if undeclared data is passed to an intent,
// and this change appears to fix the issue.
context.document["AndroidManifest.xml"].use { document ->
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val document = editor.file
// A xml regular-expression would probably work better than this manual searching.
val manifestNodes = document.getElementsByTagName("manifest").item(0).childNodes
for (i in 0..manifestNodes.length) {

View File

@ -1,7 +1,6 @@
package app.revanced.util
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.util.Document
import app.revanced.patcher.util.DomFileEditor
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Node
@ -50,7 +49,7 @@ fun ResourceContext.copyResources(
sourceResourceDirectory: String,
vararg resources: ResourceGroup,
) {
val targetResourceDirectory = this.get("res", false)
val targetResourceDirectory = this.get("res")
for (resourceGroup in resources) {
resourceGroup.resources.forEach { resource ->
@ -86,28 +85,23 @@ fun ResourceContext.iterateXmlNodeChildren(
resource: String,
targetTag: String,
callback: (node: Node) -> Unit,
) = document[classLoader.getResourceAsStream(resource)!!].use {
val stringsNode = it.getElementsByTagName(targetTag).item(0).childNodes
) = xmlEditor[classLoader.getResourceAsStream(resource)!!].use { editor ->
val document = editor.file
val stringsNode = document.getElementsByTagName(targetTag).item(0).childNodes
for (i in 1 until stringsNode.length - 1) callback(stringsNode.item(i))
}
/**
* Copies the specified node of the source [Document] to the target [Document].
* @param source the source [Document].
* @param target the target [Document]-
* @return AutoCloseable that closes the [Document]s.
*/
fun String.copyXmlNode(
source: Document,
target: Document,
): AutoCloseable {
val hostNodes = source.getElementsByTagName(this).item(0).childNodes
// TODO: After the migration to the new patcher, remove the following code and replace it with the commented code below.
fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable {
val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes
val destinationNode = target.getElementsByTagName(this).item(0)
val destinationResourceFile = target.file
val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0)
for (index in 0 until hostNodes.length) {
val node = hostNodes.item(index).cloneNode(true)
target.adoptNode(node)
destinationResourceFile.adoptNode(node)
destinationNode.appendChild(node)
}
@ -117,18 +111,44 @@ fun String.copyXmlNode(
}
}
@Deprecated(
"Use copyXmlNode(Document, Document) instead.",
ReplaceWith(
"this.copyXmlNode(source.file as Document, target.file as Document)",
"app.revanced.patcher.util.Document",
"app.revanced.patcher.util.Document",
),
)
fun String.copyXmlNode(
source: DomFileEditor,
target: DomFileEditor,
) = this.copyXmlNode(source.file as Document, target.file as Document)
// /**
// * Copies the specified node of the source [Document] to the target [Document].
// * @param source the source [Document].
// * @param target the target [Document]-
// * @return AutoCloseable that closes the [Document]s.
// */
// fun String.copyXmlNode(
// source: Document,
// target: Document,
// ): AutoCloseable {
// val hostNodes = source.getElementsByTagName(this).item(0).childNodes
//
// val destinationNode = target.getElementsByTagName(this).item(0)
//
// for (index in 0 until hostNodes.length) {
// val node = hostNodes.item(index).cloneNode(true)
// target.adoptNode(node)
// destinationNode.appendChild(node)
// }
//
// return AutoCloseable {
// source.close()
// target.close()
// }
// }
// @Deprecated(
// "Use copyXmlNode(Document, Document) instead.",
// ReplaceWith(
// "this.copyXmlNode(source.file as Document, target.file as Document)",
// "app.revanced.patcher.util.Document",
// "app.revanced.patcher.util.Document",
// ),
// )
// fun String.copyXmlNode(
// source: DomFileEditor,
// target: DomFileEditor,
// ) = this.copyXmlNode(source.file as Document, target.file as Document)
/**
* Add a resource node child.
@ -143,4 +163,4 @@ internal fun Node.addResource(
appendChild(resource.serialize(ownerDocument, resourceCallback))
}
internal fun Document?.getNode(tagName: String) = this!!.getElementsByTagName(tagName).item(0)
internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0)