Поддержка Markdown в Android TextView

Есть ли способ включить TextView для обнаружения меток разметки и визуализации текста? Более конкретно, мое приложение содержит TextView в котором пользователи могут предоставить описание, и часто они будут использовать уценку для форматирования их описания. К сожалению, текст не отображается, и вместо этого мы видим все теги, выписанные в textview.

Ответ 1

Там нет встроенной поддержки Markdown в Android SDK. Вы должны будете использовать LIB как markdown4j или CommonMark.

Ответ 2

Нет никакой наследующей поддержки уценки в textview, однако, если вам нужна простая реализация markdown-lite с помощью простого соответствия "regexp", этот раздел из моего "загрузочного файла из корневой папки проекта" в https://github.com/mofosyne/instantReadmeApp поможет.

Обратите внимание, что это не удаляет разметку в тексте, только стилирует строки по-разному. Это может быть хорошим или плохим, в зависимости от вашего приложения.

О, и хорошая вещь? Он стирается в собственном текстовом виде, поэтому текст по-прежнему можно выбрать, как обычный текст.

В частности, эта строка: https://github.com/mofosyne/instantReadmeApp/blob/master/app/src/main/java/com/github/mofosyne/instantreadme/ReadMe.java#L137

Немного изменен ниже: private void updateMainDisplay(String text) для private void style_psudomarkdown_TextView(String text, TextView textview_input), поэтому вы можете использовать ту же функцию для разных текстовых просмотров

"""

/*
    Text Styler
    A crappy psudo markdown styler. Could do with a total revamp.
 */

/*
* Styling the textview for easier readability
* */
private void style_psudomarkdown_TextView(String text, TextView textview_input) {
    //TextView mTextView = (TextView) findViewById(R.id.readme_info);
    TextView mTextView = textview_input;

    // Let update the main display
    // Needs to set as spannable otherwise http://stackoverflow.com/questions/16340681/fatal-exception-string-cant-be-cast-to-spannable
    mTextView.setText(text, TextView.BufferType.SPANNABLE);
    // Let prettify it!
    changeLineinView_TITLESTYLE(mTextView, "# ", 0xfff4585d, 2f); // Primary Header
    changeLineinView(mTextView, "\n# ", 0xFFF4A158, 1.5f); // Secondary Header
    changeLineinView(mTextView, "\n## ", 0xFFF4A158, 1.2f); // Secondary Header
    changeLineinView(mTextView, "\n---", 0xFFF4A158, 1.2f); // Horizontal Rule
    changeLineinView(mTextView, "\n>",   0xFF89e24d, 0.9f); // Block Quotes
    changeLineinView(mTextView, "\n - ", 0xFFA74DE3, 1f);   // Classic Markdown List
    changeLineinView(mTextView, "\n- ", 0xFFA74DE3, 1f);   // NonStandard List

    //spanSetterInView(String startTarget, String endTarget, int typefaceStyle, String fontFamily,TextView tv, int colour, float size)
    // Limitation of spanSetterInView. Well its not a regular expression... so can't exactly have * list, and *bold* at the same time.
    spanSetterInView(mTextView, "\n'''\n", "\n'''\n",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.8f, false); // fenced code Blocks ( endAtLineBreak=false since this is a multiline block operator)
    spanSetterInView(mTextView,   " **"  ,     "** ",   Typeface.BOLD,        "",  0xFF89e24d,  1f, true); // Bolding
    spanSetterInView(mTextView,    " *"  ,      "* ",   Typeface.ITALIC,      "",  0xFF4dd8e2,  1f, true); // Italic
    spanSetterInView(mTextView,  " ***"  ,    "*** ",   Typeface.BOLD_ITALIC, "",  0xFF4de25c,  1f, true); // Bold and Italic
    spanSetterInView(mTextView,    " '"  ,      "' ",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.8f, true); // inline code
    spanSetterInView(mTextView, "\n    " ,      "\n",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.7f, true); // classic indented code
}

private void changeLineinView(TextView tv, String target, int colour, float size) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    while (true) {
        startSpan = vString.indexOf(target, endSpan-1);     // ([email protected]#$%) I want to check a character behind in case it is a newline
        endSpan = vString.indexOf("\n", startSpan+1);       // But at the same time, I do not want to read the point found by startSpan. This is since startSpan may point to a initial newline.
        ForegroundColorSpan foreColour = new ForegroundColorSpan(colour);
        // Need a NEW span object every loop, else it just moves the span
        // Fix: -1 in startSpan or endSpan, indicates that the indexOf has already searched the entire string with not valid match (Lack of endspan check, occoured because of the inclusion of endTarget, which added extra complications)
        if ( (startSpan < 0) || ( endSpan < 0 ) ) break;// Need a NEW span object every loop, else it just moves the span
        // Need to make sure that start range is always smaller than end range. (Solved! Refer to few lines above with ([email protected]#$%) )
        if (endSpan > startSpan) {
            //endSpan = startSpan + target.length();
            spanRange.setSpan(foreColour, startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Also wannna bold the span too
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(Typeface.BOLD), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    tv.setText(spanRange);
}

private void changeLineinView_TITLESTYLE(TextView tv, String target, int colour, float size) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    /*
    * Had to do this, since there is something wrong with this overlapping the "##" detection routine
    * Plus you only really need one title.
     */
    //while (true) {
    startSpan = vString.substring(0,target.length()).indexOf(target, endSpan-1); //substring(target.length()) since we only want the first line
    endSpan = vString.indexOf("\n", startSpan+1);
    ForegroundColorSpan foreColour = new ForegroundColorSpan(colour);
    // Need a NEW span object every loop, else it just moves the span
        /*
        if (startSpan < 0)
            break;
            */
    if ( !(startSpan < 0) ) { // hacky I know, but its to cater to the case where there is no header text
        // Need to make sure that start range is always smaller than end range.
        if (endSpan > startSpan) {
            //endSpan = startSpan + target.length();
            spanRange.setSpan(foreColour, startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Also wannna bold the span too
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    //}
    tv.setText(spanRange);
}


private void spanSetterInView(TextView tv, String startTarget, String endTarget, int typefaceStyle, String fontFamily, int colour, float size, boolean endAtLineBreak) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    while (true) {
        startSpan = vString.indexOf(startTarget, endSpan-1);     // ([email protected]#$%) I want to check a character behind in case it is a newline
        endSpan = vString.indexOf(endTarget, startSpan+1+startTarget.length());     // But at the same time, I do not want to read the point found by startSpan. This is since startSpan may point to a initial newline. We also need to avoid the first patten matching a token from the second pattern.
        // Since this is pretty powerful, we really want to avoid overmatching it, and limit any problems to a single line. Especially if people forget to type in the closing symbol (e.g. * in bold)
        if (endAtLineBreak){
            int endSpan_linebreak = vString.indexOf("\n", startSpan+1+startTarget.length());
            if ( endSpan_linebreak < endSpan ) { endSpan = endSpan_linebreak; }
        }
        // Fix: -1 in startSpan or endSpan, indicates that the indexOf has already searched the entire string with not valid match (Lack of endspan check, occoured because of the inclusion of endTarget, which added extra complications)
        if ( (startSpan < 0) || ( endSpan < 0 ) ) break;// Need a NEW span object every loop, else it just moves the span
        // We want to also include the end "** " characters
        endSpan += endTarget.length();
        // If all is well, we shall set the styles and etc...
        if (endSpan > startSpan) {// Need to make sure that start range is always smaller than end range. (Solved! Refer to few lines above with ([email protected]#$%) )
            spanRange.setSpan(new ForegroundColorSpan(colour), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(typefaceStyle), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Default to normal font family if settings is empty
            if( !fontFamily.equals("") )  spanRange.setSpan(new TypefaceSpan(fontFamily), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    tv.setText(spanRange);
}

"""

Вышеупомянутая реализация поддерживает только до 2 заголовков (но вы можете легко изменить регулярное выражение для поддержки более двух уровней заголовков).

Это серия текстового представления на основе регулярного выражения, состоящего из двух функций для регулярного выражения, которая всегда соответствует строке changeLineinView() и changeLineinView_TITLESTYLE()

Для функции multiline spanning spanSetterInView() работает с ней.

Таким образом, расширение его в соответствии с вашей целью, если у вас есть регулярное выражение, которое не сталкивается с каким-либо другим синтаксисом, было бы возможно.

Ударный синтаксис:

Это поддерживаемый синтаксис. Невозможно поддерживать полную уценку, поскольку это всего лишь небольшая взломанная реализация. Но любезно удобно для без излишеств, который легко набирать на клавиатуре мобильного телефона.

# H1 only in first line (Due to technical hacks used)

## H2 headers as usual

## Styling
Like: *italic* **bold** ***bold_italic***

## Classic List
 - list item 1
 - list item 2

## Nonstandard List Syntax
- list item 1
- list item 2

## Block Quotes
> Quoted stuff

## codes
here is inline 'literal' codes. Must have space around it.

    '''
    codeblocks
    Good for ascii art
    '''

    Or 4 space code indent like classic markdown.

Ответ 3

Я могу рекомендовать MarkdownView. Я использую его для загрузки файлов уценки из папки активов.

Если это кому-нибудь поможет, вот моя реализация...

В моем макете:

<us.feras.mdv.MarkdownView
    android:id="@+id/descriptionMarkdownView"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    app:layout_constraintTop_toBottomOf="@id/thumbnailImageView"
    app:layout_constraintStart_toEndOf="@id/guidelineStart"
    app:layout_constraintEnd_toEndOf="@id/guidelineEnd"
    app:layout_constraintBottom_toTopOf="@id/parent"/>

В моей Activity:

val cssPath = "file:///android_asset/markdown.css"
val markdownPath = "file:///android_asset/markdown/filename.md"
descriptionMarkdownView.loadMarkdownFile(markdownPath, cssPath)

Ответ 4

Я понимаю, что вы хотите преобразовать String содержащую разметку Markdown, в форматированную CharSequence которую вы можете использовать в TextView. Два варианта, о которых я знаю, это:

  • Обход: используйте собственную библиотеку C для анализа текста. К сожалению, проект кажется мертвым.
  • commonmark-spannable-android: чистая java, основанная на очень хорошей библиотеке commonmark-java.

Я использовал и то, и другое, и, на мой взгляд, второе лучше: не нужно иметь дело с собственными архитектурами, меньшим APK, а производительность неплохая (примерно в 2 раза медленнее в моем случае, с более чем достаточно)

Обновление: найден другой вариант (он тот, который я использую сейчас):

  • Markwon: Чистая java, также использующая commonmark-java в качестве парсера, с дополнительной поддержкой для изображений и таблиц

Ответ 5

Взгляните на библиотеку commonmark-java. Я не пробовал это сам, но я думаю, что вы могли бы заставить его работать в вашем случае

Ответ 6

Я следил за этим постом с прошлой пятницы и протестировал многие предложенные здесь библиотеки Markdown - этот вопрос и эти ответы были в основном лучшим источником информации по теме, которую я мог найти в Интернете.

Два из них привлекли мое внимание больше всего, MarkdownView и Markwon, но с первым было легче иметь дело, чем со вторым, и поэтому я использовал его для расширения возможностей приложения для создания заметок в комнате с помощью форматирования Markdown (что было моей главной личной целью).

Если вы хотите иметь предварительный просмотр Markdown в реальном времени, вы можете просто использовать этот пример активности, предоставляемый самой библиотекой, и, среди других вариантов, если вам нужно адаптировать свою собственную активность к ней, я предлагаю вам добавить следующие фрагменты кода в ваш проект:

  • implementation 'us.feras.mdv:markdownview:1.1.0'
  • private MarkdownView markdownView;
  • markdownView = findViewById(R.id.markdownView);
  • udateMarkdownView();
  • private void updateMarkdownView() { markdownView.loadMarkdown(note_content.getText().toString()); }

Здесь вы найдете пример, который я поместил на GitHub, в котором вы можете увидеть рабочий проект отдельно от примеров, которые сама библиотека дает нам в качестве примеров.

Ответ 7

Если вы хотите отображать HTML, вы можете использовать Html.fromHtml("your string"), для получения дополнительных ресурсов на Strings in Android проверьте эту ссылку