Skip to content

Commit

Permalink
Integrate KotliTeX via MathTagHandler.
Browse files Browse the repository at this point in the history
This implementation is heavily based on #3194, including Akshay's custom
fork (which was re-forked and slightly patched to work in the latest
Oppia Android version).
  • Loading branch information
BenHenning committed Dec 17, 2021
1 parent b5ba0ba commit 6e511d7
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 33 deletions.
8 changes: 8 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ git_repository(
remote = "https://github.com/oppia/androidsvg",
)

# A custom fork of KotliTeX that removes resources artifacts that break the build, and updates the
# min target SDK version to be compatible with Oppia.
git_repository(
name = "kotlitex",
commit = "26d3eb4cc148e6dae198f96a23c29d6c05cbcc56",
remote = "https://github.com/oppia/kotlitex",
)

bind(
name = "databinding_annotation_processor",
actual = "//tools/android:compiler_annotation_processor",
Expand Down
2 changes: 1 addition & 1 deletion domain/src/main/assets/GJ2rLXRKD5hw_1.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"page_contents": {
"subtitled_html": {
"content_id": "content",
"html": "<p>Description of subtopic is here.</p>.<p>This is sample subtopic with dummy content related to Fractions.<p>Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection."
"html": "<p>Description of subtopic is here.</p>.<p>This is sample subtopic with dummy content related to Fractions: <oppia-noninteractive-math math_content-with-value=\"{&amp;quot;raw_latex&amp;quot;:&amp;quot;\\\\frac{6}{2}&amp;quot;}\"></oppia-noninteractive-math><p>Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection."
},
"recorded_voiceovers": {
"voiceovers_mapping": {
Expand Down
2 changes: 1 addition & 1 deletion domain/src/main/assets/GJ2rLXRKD5hw_1.textproto
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
subtopic_title: "What is a Fraction?"
page_contents {
html: "<p>Description of subtopic is here.</p>.<p>This is sample subtopic with dummy content related to Fractions.<p>Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection."
html: "<p>Description of subtopic is here.</p>.<p>This is sample subtopic with dummy content related to Fractions: <oppia-noninteractive-math math_content-with-value=\"{&amp;quot;raw_latex&amp;quot;:&amp;quot;\\frac{6}{2}&amp;quot;}\"></oppia-noninteractive-math><p>Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection."
content_id: "content"
}
recorded_voiceover {
Expand Down
9 changes: 9 additions & 0 deletions third_party/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ android_library(

android_library(
name = "robolectric_android-all",
testonly = True,
visibility = ["//visibility:public"],
exports = [
"@robolectric//bazel:android-all",
Expand All @@ -73,6 +74,14 @@ java_library(
],
)

android_library(
name = "io_github_karino2_kotlitex",
visibility = ["//visibility:public"],
exports = [
"@kotlitex//kotlitex",
],
)

# Define a separate target for the Glide annotation processor compiler. Unfortunately, this library
# can't encapsulate all of Glide (i.e. by exporting the main Glide dependency) since that includes
# Android assets which java_library targets do not export.
Expand Down
1 change: 1 addition & 0 deletions utility/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ kt_android_library(
"//third_party:com_github_bumptech_glide_glide",
"//third_party:com_google_guava_guava",
"//third_party:glide_compiler",
"//third_party:io_github_karino2_kotlitex",
"//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
"//utility/src/main/java/org/oppia/android/util/caching:annotations",
"//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
Expand Down
1 change: 1 addition & 0 deletions utility/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03',
'androidx.work:work-runtime-ktx:2.4.0',
'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d',
'com.github.oppia:kotlitex:26d3eb4cc148e6dae198f96a23c29d6c05cbcc56',
'com.github.bumptech.glide:glide:4.11.0',
'com.google.dagger:dagger:2.24',
'com.google.firebase:firebase-analytics-ktx:17.5.0',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.oppia.android.util.parser.html

import android.content.Context
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
Expand All @@ -12,6 +13,7 @@ import javax.inject.Inject

/** Html Parser to parse custom Oppia tags with Android-compatible versions. */
class HtmlParser private constructor(
private val context: Context,
private val urlImageParserFactory: UrlImageParser.Factory,
private val gcsResourceName: String,
private val entityType: String,
Expand All @@ -32,7 +34,6 @@ class HtmlParser private constructor(
}
private val bulletTagHandler by lazy { BulletTagHandler() }
private val imageTagHandler by lazy { ImageTagHandler(consoleLogger) }
private val mathTagHandler by lazy { MathTagHandler(consoleLogger) }

/**
* Parses a raw HTML string with support for custom Oppia tags.
Expand Down Expand Up @@ -84,7 +85,7 @@ class HtmlParser private constructor(
htmlContentTextView, gcsResourceName, entityType, entityId, imageCenterAlign
)
val htmlSpannable = CustomHtmlContentHandler.fromHtml(
htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards)
htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards, htmlContentTextView)
)

val spannableBuilder = CustomBulletSpan.replaceBulletSpan(
Expand All @@ -99,12 +100,14 @@ class HtmlParser private constructor(
}

private fun computeCustomTagHandlers(
supportsConceptCards: Boolean
supportsConceptCards: Boolean,
htmlContentTextView: TextView
): Map<String, CustomHtmlContentHandler.CustomTagHandler> {
val handlersMap = mutableMapOf<String, CustomHtmlContentHandler.CustomTagHandler>()
handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler
handlersMap[CUSTOM_IMG_TAG] = imageTagHandler
handlersMap[CUSTOM_MATH_TAG] = mathTagHandler
handlersMap[CUSTOM_MATH_TAG] =
MathTagHandler(consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat())
if (supportsConceptCards) {
handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler
}
Expand Down Expand Up @@ -143,7 +146,8 @@ class HtmlParser private constructor(
/** Factory for creating new [HtmlParser]s. */
class Factory @Inject constructor(
private val urlImageParserFactory: UrlImageParser.Factory,
private val consoleLogger: ConsoleLogger
private val consoleLogger: ConsoleLogger,
private val context: Context
) {
/**
* Returns a new [HtmlParser] with the specified entity type and ID for loading images, and an
Expand All @@ -157,6 +161,7 @@ class HtmlParser private constructor(
customOppiaTagActionListener: CustomOppiaTagActionListener? = null
): HtmlParser {
return HtmlParser(
context,
urlImageParserFactory,
gcsResourceName,
entityType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.oppia.android.util.parser.html

import android.content.res.AssetManager
import android.text.Editable
import android.text.Spannable
import android.text.style.ImageSpan
import io.github.karino2.kotlitex.view.MathExpressionSpan
import org.json.JSONObject
import org.oppia.android.util.logging.ConsoleLogger
import org.xml.sax.Attributes
Expand All @@ -16,7 +18,9 @@ private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value"
* [CustomHtmlContentHandler].
*/
class MathTagHandler(
private val consoleLogger: ConsoleLogger
private val consoleLogger: ConsoleLogger,
private val assetManager: AssetManager,
private val lineHeight: Float
) : CustomHtmlContentHandler.CustomTagHandler {
override fun handleTag(
attributes: Attributes,
Expand All @@ -29,39 +33,57 @@ class MathTagHandler(
val content = MathContent.parseMathContent(
attributes.getJsonObjectValue(CUSTOM_MATH_SVG_PATH_ATTRIBUTE)
)
if (content != null) {
// Insert an image span where the custom tag currently is to load the SVG. In the future, this
// could also load a LaTeX span, instead. Note that this approach is based on Android's Html
// parser.
val drawable =
imageRetriever.loadDrawable(
content.svgFilename,
CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE
val newSpan = when (content) {
is MathContent.MathAsSvg -> {
ImageSpan(
imageRetriever.loadDrawable(
content.svgFilename,
CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE
),
content.svgFilename
)
val (startIndex, endIndex) = output.run {
// Use a control character to ensure that there's at least 1 character on which to "attach"
// the image when rendering the HTML.
val startIndex = length
append('\uFFFC')
return@run startIndex to length
}
output.setSpan(
ImageSpan(drawable, content.svgFilename),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
} else consoleLogger.e("MathTagHandler", "Failed to parse math tag")
is MathContent.MathAsLatex -> {
MathExpressionSpan(content.rawLatex, lineHeight, assetManager, isMathMode = true)
}
null -> {
consoleLogger.e("MathTagHandler", "Failed to parse math tag")
return
}
}

// Insert an image span where the custom tag currently is to load the SVG/LaTeX span. Note that
// this approach is based on Android's HTML parser.
val (startIndex, endIndex) = output.run {
// Use a control character to ensure that there's at least 1 character on which to
// "attach" the image when rendering the HTML.
val startIndex = length
append('\uFFFC')
return@run startIndex to length
}
output.setSpan(
newSpan,
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}

private data class MathContent(val rawLatex: String, val svgFilename: String) {
private sealed class MathContent {
data class MathAsSvg(val svgFilename: String) : MathContent()

data class MathAsLatex(val rawLatex: String) : MathContent()

companion object {
internal fun parseMathContent(obj: JSONObject?): MathContent? {
// Kotlitex expects escaped backslashes.
val rawLatex = obj?.getOptionalString("raw_latex")
val svgFilename = obj?.getOptionalString("svg_filename")
return if (rawLatex != null && svgFilename != null) {
MathContent(rawLatex, svgFilename)
} else null
return when {
svgFilename != null -> MathAsSvg(svgFilename)
rawLatex != null -> MathAsLatex(rawLatex)
else -> null
}
}

/**
Expand Down

0 comments on commit 6e511d7

Please sign in to comment.