main 7ba9a5d18f59 cached
105 files
298.4 KB
79.4k tokens
1 requests
Download .txt
Showing preview only (333K chars total). Download the full file or copy to clipboard to get everything.
Repository: zach-klippenstein/compose-richtext
Branch: main
Commit: 7ba9a5d18f59
Files: 105
Total size: 298.4 KB

Directory structure:
gitextract_y6p53me7/

├── .github/
│   └── workflows/
│       ├── android.yml
│       ├── docs.yml
│       └── publish.yml
├── .gitignore
├── .idea/
│   └── codeStyles/
│       ├── Project.xml
│       └── codeStyleConfig.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android-sample/
│   ├── build.gradle.kts
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── com/
│           │       └── zachklipp/
│           │           └── richtext/
│           │               └── sample/
│           │                   ├── Demo.kt
│           │                   ├── LazyMarkdownSample.kt
│           │                   ├── MarkdownSample.kt
│           │                   ├── RichTextSample.kt
│           │                   ├── SampleActivity.kt
│           │                   ├── SampleLauncher.kt
│           │                   ├── SampleTheme.kt
│           │                   ├── ScreenPreview.kt
│           │                   └── TextDemo.kt
│           └── res/
│               ├── drawable/
│               │   └── ic_launcher_background.xml
│               ├── drawable-v24/
│               │   └── ic_launcher_foreground.xml
│               ├── mipmap-anydpi-v26/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               └── values/
│                   ├── colors.xml
│                   ├── strings.xml
│                   └── styles.xml
├── build.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               ├── Dependencies.kt
│               └── richtext-kmp-library.gradle.kts
├── desktop-sample/
│   ├── build.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── desktop/
│                               ├── MarkdownSampleApp.kt
│                               └── RichTextSampleApp.kt
├── docs/
│   ├── index.md
│   ├── richtext-commonmark.md
│   ├── richtext-markdown.md
│   ├── richtext-ui-material.md
│   ├── richtext-ui-material3.md
│   └── richtext-ui.md
├── gen_dokka_docs.sh
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── mkdocs.yml
├── richtext-commonmark/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── commonmark/
│       │                       ├── CommonMarkdownParseOptions.kt
│       │                       └── Markdown.kt
│       ├── jvmAndroidMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── commonmark/
│       │                       └── AstNodeConvert.kt
│       └── jvmAndroidTest/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── commonmark/
│                               └── AstNodeConvertKtTest.kt
├── richtext-markdown/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   ├── AndroidManifest.xml
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── markdown/
│       │                       ├── HtmlBlock.kt
│       │                       └── MarkdownImage.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── markdown/
│       │                       ├── BasicMarkdown.kt
│       │                       ├── HtmlBlock.kt
│       │                       ├── MarkdownImage.kt
│       │                       ├── MarkdownRichText.kt
│       │                       ├── RenderTable.kt
│       │                       ├── TraverseUtils.kt
│       │                       └── node/
│       │                           ├── AstNode.kt
│       │                           ├── AstNodeLinks.kt
│       │                           ├── AstNodeType.kt
│       │                           └── AstTable.kt
│       └── jvmMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── markdown/
│                               ├── HtmlBlock.kt
│                               └── RemoteImage.kt
├── richtext-ui/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   ├── AndroidManifest.xml
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       └── CodeBlock.android.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       ├── BasicRichText.kt
│       │                       ├── BlockQuote.kt
│       │                       ├── CodeBlock.kt
│       │                       ├── FormattedList.kt
│       │                       ├── Heading.kt
│       │                       ├── HorizontalRule.kt
│       │                       ├── InfoPanel.kt
│       │                       ├── RichTextLocals.kt
│       │                       ├── RichTextScope.kt
│       │                       ├── RichTextStyle.kt
│       │                       ├── RichTextThemeConfiguration.kt
│       │                       ├── RichTextThemeProvider.kt
│       │                       ├── SimpleTableLayout.kt
│       │                       ├── Table.kt
│       │                       ├── string/
│       │                       │   ├── InlineContent.kt
│       │                       │   ├── RichTextString.kt
│       │                       │   └── Text.kt
│       │                       └── util/
│       │                           ├── ConditionalTapGestureDetector.kt
│       │                           └── UUID.kt
│       ├── jvmAndroidMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       └── util/
│       │                           └── UUID.kt
│       └── jvmMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── CodeBlock.desktop.kt
├── richtext-ui-material/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── AndroidManifest.xml
│       └── commonMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── material/
│                                   └── RichText.kt
├── richtext-ui-material3/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── AndroidManifest.xml
│       └── commonMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── material3/
│                                   └── RichText.kt
└── settings.gradle.kts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/android.yml
================================================
name: Android CI

on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: gradle/wrapper-validation-action@v1
      - name: set up JDK 21
        uses: actions/setup-java@v1
        with:
          java-version: 21
      - uses: actions/cache@v4
        with:
          path: ~/.gradle/caches
          key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }}
          restore-keys: gradle-${{ runner.os }}-
      - run: ./gradlew build


================================================
FILE: .github/workflows/docs.yml
================================================
name: Doc Site

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v1
        with:
          java-version: 21
      - uses: actions/setup-python@v2
        with:
          python-version: 3.x
      - name: Install dependencies
        run: pip install mkdocs-material
      - name: Generate docs
        run: ./gen_dokka_docs.sh
      - run: mkdocs gh-deploy --force


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish

on:
  workflow_dispatch:

jobs:
  publish:
    name: Release build and publish
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: gradle/wrapper-validation-action@v1
      - name: set up JDK 21
        uses: actions/setup-java@v1
        with:
          java-version: 21
      - uses: actions/cache@v4
        with:
          path: ~/.gradle/caches
          key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }}
          restore-keys: gradle-${{ runner.os }}-
      - name: Release build
        run: ./gradlew :build
      - name: Publish to MavenCentral
        run: ./gradlew publishAllPublicationsToMavenCentralRepository
        env:
          GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
          GPG_PRIVATE_PASSWORD: ${{ secrets.GPG_PRIVATE_PASSWORD }}
          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}
          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}


================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build/
/captures
.externalNativeBuild
.cxx
site/
docs-gen/


================================================
FILE: .idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <code_scheme name="Project" version="173">
    <option name="OTHER_INDENT_OPTIONS">
      <value>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </value>
    </option>
    <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
    <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
    <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
      <value />
    </option>
    <option name="IMPORT_LAYOUT_TABLE">
      <value>
        <package name="" withSubpackages="true" static="false" />
        <emptyLine />
        <package name="" withSubpackages="true" static="true" />
      </value>
    </option>
    <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
    <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
    <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
    <option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
    <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
    <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
    <option name="ALIGN_MULTILINE_FOR" value="false" />
    <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
    <option name="CALL_PARAMETERS_WRAP" value="1" />
    <option name="METHOD_PARAMETERS_WRAP" value="1" />
    <option name="RESOURCE_LIST_WRAP" value="1" />
    <option name="EXTENDS_LIST_WRAP" value="1" />
    <option name="THROWS_LIST_WRAP" value="1" />
    <option name="EXTENDS_KEYWORD_WRAP" value="1" />
    <option name="THROWS_KEYWORD_WRAP" value="1" />
    <option name="METHOD_CALL_CHAIN_WRAP" value="5" />
    <option name="BINARY_OPERATION_WRAP" value="5" />
    <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
    <option name="TERNARY_OPERATION_WRAP" value="1" />
    <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
    <option name="FOR_STATEMENT_WRAP" value="1" />
    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
    <option name="ASSIGNMENT_WRAP" value="1" />
    <option name="WRAP_COMMENTS" value="true" />
    <option name="ASSERT_STATEMENT_WRAP" value="1" />
    <option name="IF_BRACE_FORCE" value="1" />
    <option name="DOWHILE_BRACE_FORCE" value="1" />
    <option name="WHILE_BRACE_FORCE" value="1" />
    <option name="METHOD_ANNOTATION_WRAP" value="1" />
    <option name="CLASS_ANNOTATION_WRAP" value="1" />
    <option name="FIELD_ANNOTATION_WRAP" value="1" />
    <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
    <option name="VARIABLE_ANNOTATION_WRAP" value="1" />
    <option name="ENUM_CONSTANTS_WRAP" value="1" />
    <AndroidXmlCodeStyleSettings>
      <option name="LAYOUT_SETTINGS">
        <value>
          <option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
          <option name="INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE" value="true" />
        </value>
      </option>
    </AndroidXmlCodeStyleSettings>
    <GroovyCodeStyleSettings>
      <option name="ALIGN_MULTILINE_LIST_OR_MAP" value="false" />
      <option name="ALIGN_NAMED_ARGS_IN_MAP" value="false" />
    </GroovyCodeStyleSettings>
    <JavaCodeStyleSettings>
      <option name="CLASS_NAMES_IN_JAVADOC" value="3" />
      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
      <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
      <option name="IMPORT_LAYOUT_TABLE">
        <value>
          <package name="" withSubpackages="true" static="false" />
          <emptyLine />
          <package name="" withSubpackages="true" static="true" />
        </value>
      </option>
      <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
      <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
      <option name="JD_P_AT_EMPTY_LINES" value="false" />
      <option name="JD_DO_NOT_WRAP_ONE_LINE_COMMENTS" value="true" />
      <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
      <option name="JD_KEEP_EMPTY_RETURN" value="false" />
      <option name="JD_PRESERVE_LINE_FEEDS" value="true" />
    </JavaCodeStyleSettings>
    <JetCodeStyleSettings>
      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="999" />
      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="999" />
      <option name="IMPORT_NESTED_CLASSES" value="true" />
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </JetCodeStyleSettings>
    <ADDITIONAL_INDENT_OPTIONS fileType="php">
      <option name="INDENT_SIZE" value="2" />
      <option name="CONTINUATION_INDENT_SIZE" value="4" />
      <option name="TAB_SIZE" value="2" />
    </ADDITIONAL_INDENT_OPTIONS>
    <ADDITIONAL_INDENT_OPTIONS fileType="scala">
      <option name="INDENT_SIZE" value="2" />
      <option name="CONTINUATION_INDENT_SIZE" value="2" />
      <option name="TAB_SIZE" value="2" />
    </ADDITIONAL_INDENT_OPTIONS>
    <codeStyleSettings language="CSS">
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="CoffeeScript">
      <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="METHOD_PARAMETERS_WRAP" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="Groovy">
      <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
      <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="ALIGN_MULTILINE_FOR" value="false" />
      <option name="CALL_PARAMETERS_WRAP" value="1" />
      <option name="METHOD_PARAMETERS_WRAP" value="1" />
      <option name="EXTENDS_LIST_WRAP" value="1" />
      <option name="THROWS_LIST_WRAP" value="1" />
      <option name="EXTENDS_KEYWORD_WRAP" value="1" />
      <option name="THROWS_KEYWORD_WRAP" value="1" />
      <option name="METHOD_CALL_CHAIN_WRAP" value="5" />
      <option name="BINARY_OPERATION_WRAP" value="5" />
      <option name="TERNARY_OPERATION_WRAP" value="1" />
      <option name="FOR_STATEMENT_WRAP" value="1" />
      <option name="ASSIGNMENT_WRAP" value="1" />
      <option name="ASSERT_STATEMENT_WRAP" value="1" />
      <option name="IF_BRACE_FORCE" value="1" />
      <option name="WHILE_BRACE_FORCE" value="1" />
      <option name="METHOD_ANNOTATION_WRAP" value="1" />
      <option name="CLASS_ANNOTATION_WRAP" value="1" />
      <option name="FIELD_ANNOTATION_WRAP" value="1" />
      <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
      <option name="VARIABLE_ANNOTATION_WRAP" value="1" />
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="JAVA">
      <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
      <option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
      <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
      <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="ALIGN_MULTILINE_FOR" value="false" />
      <option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
      <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
      <option name="CALL_PARAMETERS_WRAP" value="1" />
      <option name="METHOD_PARAMETERS_WRAP" value="1" />
      <option name="RESOURCE_LIST_WRAP" value="1" />
      <option name="EXTENDS_LIST_WRAP" value="1" />
      <option name="THROWS_LIST_WRAP" value="1" />
      <option name="EXTENDS_KEYWORD_WRAP" value="1" />
      <option name="THROWS_KEYWORD_WRAP" value="1" />
      <option name="METHOD_CALL_CHAIN_WRAP" value="5" />
      <option name="BINARY_OPERATION_WRAP" value="5" />
      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
      <option name="TERNARY_OPERATION_WRAP" value="1" />
      <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
      <option name="FOR_STATEMENT_WRAP" value="1" />
      <option name="ARRAY_INITIALIZER_WRAP" value="1" />
      <option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
      <option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
      <option name="ASSIGNMENT_WRAP" value="1" />
      <option name="WRAP_COMMENTS" value="true" />
      <option name="ASSERT_STATEMENT_WRAP" value="1" />
      <option name="IF_BRACE_FORCE" value="1" />
      <option name="DOWHILE_BRACE_FORCE" value="1" />
      <option name="WHILE_BRACE_FORCE" value="1" />
      <option name="METHOD_ANNOTATION_WRAP" value="1" />
      <option name="CLASS_ANNOTATION_WRAP" value="1" />
      <option name="FIELD_ANNOTATION_WRAP" value="1" />
      <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
      <option name="VARIABLE_ANNOTATION_WRAP" value="1" />
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="JSON">
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="JavaScript">
      <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="ALIGN_MULTILINE_FOR" value="false" />
      <option name="CALL_PARAMETERS_WRAP" value="1" />
      <option name="METHOD_PARAMETERS_WRAP" value="1" />
      <option name="BINARY_OPERATION_WRAP" value="5" />
      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
      <option name="TERNARY_OPERATION_WRAP" value="1" />
      <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
      <option name="FOR_STATEMENT_WRAP" value="1" />
      <option name="ARRAY_INITIALIZER_WRAP" value="1" />
      <option name="ASSIGNMENT_WRAP" value="1" />
      <option name="IF_BRACE_FORCE" value="1" />
      <option name="DOWHILE_BRACE_FORCE" value="1" />
      <option name="WHILE_BRACE_FORCE" value="1" />
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="SQL">
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="TypeScript">
      <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="ALIGN_MULTILINE_FOR" value="false" />
      <option name="CALL_PARAMETERS_WRAP" value="1" />
      <option name="METHOD_PARAMETERS_WRAP" value="1" />
      <option name="EXTENDS_LIST_WRAP" value="1" />
      <option name="EXTENDS_KEYWORD_WRAP" value="1" />
      <option name="BINARY_OPERATION_WRAP" value="5" />
      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
      <option name="TERNARY_OPERATION_WRAP" value="1" />
      <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
      <option name="FOR_STATEMENT_WRAP" value="1" />
      <option name="ARRAY_INITIALIZER_WRAP" value="1" />
      <option name="ASSIGNMENT_WRAP" value="1" />
      <option name="WRAP_COMMENTS" value="true" />
      <option name="IF_BRACE_FORCE" value="1" />
      <option name="DOWHILE_BRACE_FORCE" value="1" />
      <option name="WHILE_BRACE_FORCE" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="XML">
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
      <arrangement>
        <rules>
          <section>
            <rule>
              <match>
                <NAME>class</NAME>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <NAME>layout</NAME>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <NAME>xmlns:android</NAME>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <NAME>xmlns:.*</NAME>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:id</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:name</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:layout_width</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:layout_height</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:layout_.*</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>app:layout_.*</NAME>
                  <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <NAME>.*(?&lt;!style)$</NAME>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <NAME>style</NAME>
              </match>
            </rule>
          </section>
        </rules>
      </arrangement>
    </codeStyleSettings>
    <codeStyleSettings language="kotlin">
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
      <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
      <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
      <option name="METHOD_ANNOTATION_WRAP" value="1" />
      <option name="FIELD_ANNOTATION_WRAP" value="1" />
      <option name="ENUM_CONSTANTS_WRAP" value="2" />
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
  </code_scheme>
</component>

================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
  </state>
</component>


================================================
FILE: CHANGELOG.md
================================================
Changelog
=========

1.0.0-alpha03
-------

This release removes the `printing` and `slideshow` modules to focus on the core Markdown and RichText functionalities. It also adds support for inline base64 images.

### Breaking Changes
- The `printing` and `slideshow` modules have been removed. If you were using them, you will need to find an alternative or use a previous version of the library.

### New Features
- **Inline Base64 Image Rendering**: Markdown images can now be rendered from inline base64-encoded data URIs.

### Updates & Maintenance
- **Dependencies Updated**:
    - Compose Multiplatform updated to `1.8.2`.
    - Commonmark updated to `0.25.0`.
    - Dokka updated to `2.0.0`.
- **Build & CI**:
    - Android Gradle Plugin and other dependencies have been updated.
    - CI now uses `actions/cache@v4`.
- **Sample App**:
    - The Android sample app has been updated to reflect the removal of the `printing` and `slideshow` modules.
    - Theme handling in the sample app has been simplified.

v0.11.0
------

_2022_02_09_

* Upgrade Coil to 2.0.0-alpha06 by @msfjarvis in https://github.com/halilozercan/compose-richtext/pull/72

## New Contributors
* @msfjarvis made their first contribution in https://github.com/halilozercan/compose-richtext/pull/72

**Full Changelog**: https://github.com/halilozercan/compose-richtext/compare/v0.10.0...v0.11.0

v0.10.0
------

_2021_12_05_

This release celebrates the release of Compose Multiplatform 1.0.0 🎉🥳

v0.9.0
------

_2021_11_20_

This release is mostly a version bump.
- Jetpack Compose: 1.1.0-beta03
- Jetbrains Compose: 1.0.0-beta5
- Kotlin: 1.5.31

Other changes:

* Fix link formatting in index page of docs by in https://github.com/halilozercan/compose-richtext/pull/60
* CodeBlock fixes in https://github.com/halilozercan/compose-richtext/pull/62
* Update CHANGELOG.md to include releases after the transfer in https://github.com/halilozercan/compose-richtext/pull/64
* Add info panels similar to bootstrap alerts #54 in https://github.com/halilozercan/compose-richtext/pull/63


**Full Changelog**: https://github.com/halilozercan/compose-richtext/compare/v0.8.1...v0.9.0

v0.8.1
------

_2021-9-11_

This release fixes JVM artifact issue #59

v0.8.0
------

_2021-9-8_

Compose Richtext goes KMP, opening RichText UI and its extensions to both Android and Desktop (#50)

Special thanks @zach-klippenstein @LouisCAD @russhwolf for their reviews and help.

* Richtext UI, Richtext UI Material, and RichText Commonmark are now KMP Compose libraries
* Slideshow, Printing remains Android only for the foreseeable future
* Updated docs
* A new CI compatible release configuration

v0.7.0
------

_2021-8-6_

* RichText UI no longer depends on Material (#45)
* A new artifact richtext-ui-material is published to easily integrate RichText for apps that use Material design.
* Upgraded compose to 1.0.1 and kotlin to 1.5.21

v0.6.0
------

_2021-7-29_

* **Compose 1.0.0 support** (#43)
* Upgrade to Gradle 7.0.2 (#40)
* Fix wrong word used. portrait -> landscape (#37 - thanks @LouisCAD)
* Repository has migrated from @zach-klippenstein to @halilozercan.
* Artifacts have moved from com.zachklipp.compose-richtext to com.halilibo.compose-richtext.
* Similarly, documentation is also now available at halilibo.com/compose-richtext

v0.5.0
------

_2021-5-18_

* **Compose Beta 7 support!** (#36)
* Fix several bugs in Table, RichTextStyle and improve InlineContent (#35 – thanks @halilozercan!)

v0.2.0
------

_2021-2-27_

* **Compose Beta 1 support!**
* Remove BulletList styling for different leading characters - Update markdown-demo.png to show new
  BulletList rendering (#28 – thanks @halilozercan!)

v0.1.0+alpha06
--------------

_2020-11-06_

* Initial release.

Thanks to @halilozercan for implementing Markdown support!

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# Compose Markdown and Rich Text

[![Maven Central](https://img.shields.io/maven-central/v/com.halilibo.compose-richtext/richtext-ui.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.halilibo.compose-richtext%22)
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)

> **Warning**
> compose-richtext library and all its modules are very experimental. The roadmap is unclear at the moment. Thanks for your patience. Fork option is available as always.

A collection of Compose libraries for working with Markdown rendering and rich text formatting.

All modules are Compose Multiplatform compatible but lacks iOS support.

----

**Documentation is available at [halilibo.com/compose-richtext](https://halilibo.com/compose-richtext).**

----

```kotlin
@Composable fun App() {
  RichText(Modifier.background(color = Color.LightGray)) {
    Heading(0, "Title")
    Text("Summary paragraph.")

    HorizontalRule()

    BlockQuote {
      Text("A wise person once said…")
    }
    
    Markdown("**Hello** `World`")
  }
}
```

## License
```
Copyright 2025 Halil Ozercan

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```


================================================
FILE: android-sample/build.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
  id("com.android.application")
  kotlin("android")
  id("org.jetbrains.compose")
  id("org.jetbrains.kotlin.plugin.compose")
}

android {
  namespace = "com.zachklipp.richtext.sample"
  compileSdk = AndroidConfiguration.compileSdk

  defaultConfig {
    minSdk = AndroidConfiguration.minSdk
    targetSdk = AndroidConfiguration.targetSdk
  }

  buildFeatures {
    compose = true
  }

  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
  }
}

kotlin {
  compilerOptions {
    jvmTarget = JvmTarget.JVM_11
  }
}

dependencies {
  implementation(project(":richtext-commonmark"))
  implementation(project(":richtext-ui-material3"))
  implementation(AndroidX.appcompat)
  implementation(Compose.activity)
  implementation(compose.foundation)
  implementation(compose.materialIconsExtended)
  implementation(compose.material3)
  implementation(compose.uiTooling)
}


================================================
FILE: android-sample/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: android-sample/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  <uses-permission android:name="android.permission.INTERNET" />

  <application
      android:allowBackup="false"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">

    <activity
        android:name=".MainActivity"
        android:configChanges="orientation|screenSize|smallestScreenSize|fontScale|density|layoutDirection"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>


================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt
================================================
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")

package com.zachklipp.richtext.sample

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.ui.BlockQuote
import com.halilibo.richtext.ui.CodeBlock
import com.halilibo.richtext.ui.FormattedList
import com.halilibo.richtext.ui.Heading
import com.halilibo.richtext.ui.HorizontalRule
import com.halilibo.richtext.ui.InfoPanel
import com.halilibo.richtext.ui.InfoPanelType
import com.halilibo.richtext.ui.ListType
import com.halilibo.richtext.ui.ListType.Ordered
import com.halilibo.richtext.ui.ListType.Unordered
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.Table
import com.halilibo.richtext.ui.material3.RichText

@Preview(widthDp = 300, heightDp = 1000)
@Composable fun RichTextDemoOnWhite() {
  Box(Modifier.background(color = Color.White)) {
    RichTextDemo()
  }
}

@Preview(widthDp = 300, heightDp = 1000)
@Composable fun RichTextDemoOnBlack() {
  CompositionLocalProvider(LocalContentColor provides Color.White) {
    Box(Modifier.background(color = Color.Black)) {
      RichTextDemo()
    }
  }
}

@Composable fun RichTextDemo(
  style: RichTextStyle? = null,
  header: String = ""
) {
  RichText(
    modifier = Modifier.padding(8.dp),
    style = style
  ) {
    Heading(0, "Paragraphs $header")
    Text("Simple paragraph.")
    Text("Paragraph with\nmultiple lines.")
    Text("Paragraph with really long line that should be getting wrapped.")
    TextPreview()

    Heading(0, "Lists")
    Heading(1, "Unordered")
    ListDemo(listType = Unordered)
    Heading(1, "Ordered")
    ListDemo(listType = Ordered)

    Heading(0, "Horizontal Line")
    Text("Above line")
    HorizontalRule()
    Text("Below line")

    Heading(0, "Code Block")
    CodeBlock(
      """
        {
          "Hello": "world!"
        }
      """.trimIndent()
    )

    Heading(0, "Block Quote")
    BlockQuote {
      Text("These paragraphs are quoted.")
      Text("More text.")
      BlockQuote {
        Text("Nested block quote.")
      }
    }

    Heading(0, "Info Panel")
    InfoPanel(InfoPanelType.Primary, "Only text primary info panel")
    InfoPanel(InfoPanelType.Success) {
      Column {
        Text("Successfully sent some data")
        HorizontalRule()
        BlockQuote {
          Text("This is a quote")
        }
      }
    }

    Heading(0, "Table")
    Table(
      modifier = Modifier.fillMaxWidth(),
      headerRow = {
        cell { Text("Column 1") }
        cell { Text("Column 2") }
      }) {
      row {
        cell { Text("Hello") }
        cell {
          CodeBlock("Foo bar")
        }
      }
      row {
        cell {
          BlockQuote {
            Text("Stuff")
          }
        }
        cell { Text("Hello world this is a really long line that is going to wrap hopefully") }
      }
    }
  }
}

@Composable private fun RichTextScope.ListDemo(listType: ListType) {
  FormattedList(listType,
    @Composable {
      Text("First list item")
      FormattedList(listType,
        @Composable { Text("Indented 1") }
      )
    },
    @Composable {
      Text("Second list item.")
      FormattedList(listType,
        @Composable { Text("Indented 2") }
      )
    }
  )
}


================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/LazyMarkdownSample.kt
================================================
package com.zachklipp.richtext.sample

import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.markdown.node.AstDocument
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.currentRichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults

@Preview
@Composable private fun LazyMarkdownSamplePreview() {
  LazyMarkdownSample()
}

@OptIn(ExperimentalLayoutApi::class)
@Composable fun LazyMarkdownSample() {
  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }
  var isDarkModeEnabled by remember { mutableStateOf(false) }
  var isWordWrapEnabled by remember { mutableStateOf(true) }
  var markdownParseOptions by remember { mutableStateOf(CommonMarkdownParseOptions.Default) }
  var isAutolinkEnabled by remember { mutableStateOf(true) }

  LaunchedEffect(isWordWrapEnabled) {
    richTextStyle = richTextStyle.copy(
      codeBlockStyle = richTextStyle.codeBlockStyle!!.copy(
        wordWrap = isWordWrapEnabled
      )
    )
  }
  LaunchedEffect(isAutolinkEnabled) {
    markdownParseOptions = markdownParseOptions.copy(
      autolink = isAutolinkEnabled
    )
  }

  val context = LocalContext.current

  SampleTheme(isDarkModeEnabled) {
    Surface {
      Column {
        // Config
        Card(elevation = CardDefaults.elevatedCardElevation()) {
          Column {
            FlowRow {
              CheckboxPreference(
                onClick = {
                  isDarkModeEnabled = !isDarkModeEnabled
                },
                checked = isDarkModeEnabled,
                label = "Dark Mode"
              )
              CheckboxPreference(
                onClick = {
                  isWordWrapEnabled = !isWordWrapEnabled
                },
                checked = isWordWrapEnabled,
                label = "Word Wrap"
              )
              CheckboxPreference(
                onClick = {
                  isAutolinkEnabled = !isAutolinkEnabled
                },
                checked = isAutolinkEnabled,
                label = "Autolink"
              )
            }

            RichTextStyleConfig(
              richTextStyle = richTextStyle,
              onChanged = { richTextStyle = it }
            )
          }
        }

        SelectionContainer {
          val parser = remember(markdownParseOptions) {
            CommonmarkAstNodeParser(markdownParseOptions)
          }

          val astNode = remember(parser) {
            parser.parse(sampleMarkdown)
          }

          ProvideToastUriHandler(context) {
            RichText(
              style = richTextStyle,
              modifier = Modifier.padding(8.dp),
            ) {
              LazyMarkdown(astNode)
            }
          }
        }
      }
    }
  }
}

/**
 * A function that renders Markdown content lazily at the top level. All markdown trees start with
 * an AstDocument. If a document is long enough, usually there are more than hundred child nodes
 * under the root. Then in turn rendering the whole content into the internal column of
 * `BasicRichText` becomes extremely inefficient. Instead, this renderer at least relieves the top
 * level rendering by turning the internal column into a LazyColumn. All other nodes below the
 * first level are rendered as usual.
 *
 * @param astNode Root node of Markdown tree. This can be obtained via a parser.
 */
@Composable
fun RichTextScope.LazyMarkdown(astNode: AstNode) {
  require(astNode.type == AstDocument) {
    "Lazy Markdown rendering requires root level node to have a type of AstDocument."
  }
  // keep the same blockSpacing
  val currentStyle = currentRichTextStyle
  val resolvedStyle = remember(currentStyle) { currentStyle.resolveDefaults() }
  val blockSpacing = with(LocalDensity.current) {
    resolvedStyle.paragraphSpacing!!.toDp()
  }
  LazyColumn(verticalArrangement = Arrangement.spacedBy(blockSpacing)) {
    var iter = astNode.links.firstChild
    while (iter != null) {
      // We need to store iter in a final variable because composition of `item` happens after
      // iteration
      val node = iter
      item {
        BasicMarkdown(node)
      }
      iter = iter.links.next
    }
  }
}

@Composable
private fun CheckboxPreference(
  onClick: () -> Unit,
  checked: Boolean,
  label: String
) {
  Row(
    Modifier.clickable(onClick = onClick),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalAlignment = Alignment.CenterVertically
  ) {
    Checkbox(
      checked = checked,
      onCheckedChange = { onClick() },
    )
    Text(label)
  }
}

private val sampleMarkdown = """
  # Demo
  Based on [this cheatsheet][cheatsheet]

  ---

  ## Headers
  ---
  # Header 1
  ## Header 2
  ### Header 3
  #### Header 4
  ##### Header 5
  ###### Header 6
  ---
  
  ## Full-bleed Image
  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)

  ## Images smaller than the width should center
  ![](https://cdn.nostr.build/p/4a84.png)
  
  On LineHeight bug, the image below goes over this text. 
  ![](https://cdn.nostr.build/p/PxZ0.jpg)

  ## Emphasis

  Emphasis, aka italics, with *asterisks* or _underscores_.

  Strong emphasis, aka bold, with **asterisks** or __underscores__.

  Combined emphasis with **asterisks and _underscores_**.

  ---

  ## Lists
  1. First ordered list item
  2. Another item
      * Unordered sub-list.
  1. Actual numbers don't matter, just that it's a number
      1. Ordered sub-list
  4. And another item.

      You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).

      To have a line break without a paragraph, you will need to use two trailing spaces.
      Note that this line is separate, but within the same paragraph.
      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)

  * Unordered list can use asterisks
  - Or minuses
  + Or pluses
<!-- -->
  2. Ordered list starting with `2.`
  3. Another item
<!-- -->
  0. Ordered list starting with `0.`
<!-- -->
  003. Ordered list starting with `003.`
<!-- -->
  -1. Starting with `-1.` should not be list


  ---

  ## Links

  [I'm an inline-style link](https://www.google.com)

  [I'm a reference-style link][Arbitrary case-insensitive reference text]

  [I'm a relative reference to a repository file](../blob/master/LICENSE)

  [You can use numbers for reference-style link definitions][1]

  Or leave it empty and use the [link text itself].
  
  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.

  ---

  ## Code

  Inline `code` has `back-ticks around` it.

  ```javascript
  var s = "JavaScript syntax highlighting";
  alert(s);
  ```

  ```python
  s = "Python syntax highlighting"
  print s
  ```

  ```java
  /**
   * Helper method to obtain a Parser with registered strike-through &amp; table extensions
   * &amp; task lists (added in 1.0.1)
   *
   * @return a Parser instance that is supported by this library
   * @since 1.0.0
   */
  @NonNull
  public static Parser createParser() {
    return new Parser.Builder()
        .extensions(Arrays.asList(
            StrikethroughExtension.create(),
            TablesExtension.create(),
            TaskListExtension.create()
        ))
        .build();
  }
  ```

  ```xml
  <ScrollView
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="?android:attr/actionBarSize">

    <TextView
      android:id="@+id/text"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="16dip"
      android:lineSpacingExtra="2dip"
      android:textSize="16sp"
      tools:text="yo\nman" />

  </ScrollView>
  ```

  ```
  No language indicated, so no syntax highlighting.
  But let's throw in a <b>tag</b>.
  ```
  
  ---

  ## Images
  
  Inline-style:
   
  ![random image](https://picsum.photos/seed/picsum/400/400)
  
  ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  
  Reference-style:
   
  ![random image][logo]
  
  [logo]: https://picsum.photos/seed/picsum2/400/400 "Text 2"

  ---

  ## Tables

  Colons can be used to align columns.

  | Tables        | Are           | Cool  |
  | ------------- |:-------------:| -----:|
  | col 3 is      | right-aligned | ${'$'}1600 |
  | col 2 is      | centered      |   ${'$'}12 |
  | zebra stripes | are neat      |    ${'$'}1 |

  There must be at least 3 dashes separating each header cell.
  The outer pipes (|) are optional, and you don't need to make the
  raw Markdown line up prettily. You can also use inline Markdown.

  Markdown | Less | Pretty
  --- | --- | ---
  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  1 | 2 | 3

  ---

  ## Blockquotes

  > Blockquotes are very handy in email to emulate reply text.
  > This line is part of the same quote.

  Quote break.

  > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.

  Nested quotes
  > Hello!
  >> And to you!

  ---

  ## Inline HTML

  ```html
  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
  ```

  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>

  ---

  ## Horizontal Rule

  Three or more...

  ---

  Hyphens (`-`)

  ***

  Asterisks (`*`)

  ___

  Underscores (`_`)


  ## License

  ```
    Copyright 2019 Dimitry Ivanov (legal@noties.io)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
  ```

  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet

  [arbitrary case-insensitive reference text]: https://www.mozilla.org
  [1]: http://slashdot.org
  [link text itself]: http://www.reddit.com
""".trimIndent()

================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt
================================================
package com.zachklipp.richtext.sample

import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.AstBlockNodeComposer
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.markdown.node.AstBlockNodeType
import com.halilibo.richtext.markdown.node.AstHeading
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.ui.Heading
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults

@Preview
@Composable private fun MarkdownSamplePreview() {
  MarkdownSample()
}

@OptIn(ExperimentalLayoutApi::class)
@Composable fun MarkdownSample() {
  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }
  var isWordWrapEnabled by remember { mutableStateOf(true) }
  var markdownParseOptions by remember { mutableStateOf(CommonMarkdownParseOptions.Default) }
  var isAutolinkEnabled by remember { mutableStateOf(true) }
  var isRtl by remember { mutableStateOf(false) }

  LaunchedEffect(isWordWrapEnabled) {
    richTextStyle = richTextStyle.copy(
      codeBlockStyle = richTextStyle.codeBlockStyle!!.copy(
        wordWrap = isWordWrapEnabled
      )
    )
  }
  LaunchedEffect(isAutolinkEnabled) {
    markdownParseOptions = markdownParseOptions.copy(
      autolink = isAutolinkEnabled
    )
  }

  val context = LocalContext.current

  CompositionLocalProvider(
    LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
  ) {
    Column {
      // Config
      Card(elevation = CardDefaults.elevatedCardElevation()) {
        Column {
          FlowRow {
            CheckboxPreference(
              onClick = {
                isWordWrapEnabled = !isWordWrapEnabled
              },
              checked = isWordWrapEnabled,
              label = "Word Wrap"
            )
            CheckboxPreference(
              onClick = {
                isAutolinkEnabled = !isAutolinkEnabled
              },
              checked = isAutolinkEnabled,
              label = "Autolink"
            )
            CheckboxPreference(
              onClick = {
                isRtl = !isRtl
              },
              checked = isRtl,
              label = "RTL Layout"
            )
          }

          RichTextStyleConfig(
            richTextStyle = richTextStyle,
            onChanged = { richTextStyle = it }
          )
        }
      }

      SelectionContainer {
        Column(Modifier.verticalScroll(rememberScrollState())) {
          val parser = remember(markdownParseOptions) {
            CommonmarkAstNodeParser(markdownParseOptions)
          }

          val astNode = remember(parser) {
            parser.parse(sampleMarkdown)
          }

          ProvideToastUriHandler(context) {
            RichText(
              style = richTextStyle,
              modifier = Modifier.padding(8.dp),
            ) {
              BasicMarkdown(astNode, HeadingAstBlockNodeComposer)
            }
          }
        }
      }
    }
  }
}

val HeadingAstBlockNodeComposer = object : AstBlockNodeComposer {
  override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean {
    return astBlockNodeType is AstHeading
  }

  @Composable override fun RichTextScope.Compose(
    astNode: AstNode,
    visitChildren: @Composable (AstNode) -> Unit
  ) {
    val headingNode = astNode.type as? AstHeading ?: return
    Column {
      Heading(level = headingNode.level) {
        visitChildren(astNode)
      }
      Text("Custom rendering is used for this heading!", fontSize = 8.sp)
    }
  }
}

@Composable
private fun CheckboxPreference(
  onClick: () -> Unit,
  checked: Boolean,
  label: String
) {
  Row(
    Modifier.clickable(onClick = onClick),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalAlignment = Alignment.CenterVertically
  ) {
    Checkbox(
      checked = checked,
      onCheckedChange = { onClick() },
    )
    Text(label)
  }
}

private val sampleMarkdown = """
  # Demo
  Based on [this cheatsheet][cheatsheet]

  ---

  ## Headers
  ---
  # Header 1
  ## Header 2
  ### Header 3
  #### Header 4
  ##### Header 5
  ###### Header 6
  ---
  
  ## Full-bleed Image
  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)

  ## Images smaller than the width should center
  ![](https://cdn.nostr.build/p/4a84.png)
  
  On LineHeight bug, the image below goes over this text. 
  ![](https://cdn.nostr.build/p/PxZ0.jpg)

  ## Emphasis

  Emphasis, aka italics, with *asterisks* or _underscores_.

  Strong emphasis, aka bold, with **asterisks** or __underscores__.

  Combined emphasis with **asterisks and _underscores_**.

  ---

  ## Lists
  1. First ordered list item
  2. Another item
      * Unordered sub-list.
  1. Actual numbers don't matter, just that it's a number
      1. Ordered sub-list
  4. And another item.

      You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).

      To have a line break without a paragraph, you will need to use two trailing spaces.
      Note that this line is separate, but within the same paragraph.
      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)

  * Unordered list can use asterisks
  - Or minuses
  + Or pluses
<!-- -->
  2. Ordered list starting with `2.`
  3. Another item
<!-- -->
  0. Ordered list starting with `0.`
<!-- -->
  003. Ordered list starting with `003.`
<!-- -->
  -1. Starting with `-1.` should not be list


  ---

  ## Links

  [I'm an inline-style link](https://www.google.com)

  [I'm a reference-style link][Arbitrary case-insensitive reference text]

  [I'm a relative reference to a repository file](../blob/master/LICENSE)

  [You can use numbers for reference-style link definitions][1]

  Or leave it empty and use the [link text itself].
  
  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.

  ---

  ## Code

  Inline `code` has `back-ticks around` it.

  ```javascript
  var s = "JavaScript syntax highlighting";
  alert(s);
  ```

  ```python
  s = "Python syntax highlighting"
  print s
  ```

  ```java
  /**
   * Helper method to obtain a Parser with registered strike-through &amp; table extensions
   * &amp; task lists (added in 1.0.1)
   *
   * @return a Parser instance that is supported by this library
   * @since 1.0.0
   */
  @NonNull
  public static Parser createParser() {
    return new Parser.Builder()
        .extensions(Arrays.asList(
            StrikethroughExtension.create(),
            TablesExtension.create(),
            TaskListExtension.create()
        ))
        .build();
  }
  ```

  ```xml
  <ScrollView
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="?android:attr/actionBarSize">

    <TextView
      android:id="@+id/text"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="16dip"
      android:lineSpacingExtra="2dip"
      android:textSize="16sp"
      tools:text="yo\nman" />

  </ScrollView>
  ```

  ```
  No language indicated, so no syntax highlighting.
  But let's throw in a <b>tag</b>.
  ```
  
  ---

  ## Images
  
  Inline-style:
   
  ![random image](https://picsum.photos/seed/picsum/400/400)
  
  ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  
  Reference-style:
   
  ![random image][logo]
  
  [logo]: https://picsum.photos/seed/picsum2/400/400 "Text 2"
  
  Base64 Inline
  
  ![][image1]

  ---

  ## Tables

  Colons can be used to align columns.

  | Tables        | Are           | Cool  |
  | ------------- |:-------------:| -----:|
  | col 3 is      | right-aligned | ${'$'}1600 |
  | col 2 is      | centered      |   ${'$'}12 |
  | zebra stripes | are neat      |    ${'$'}1 |

  There must be at least 3 dashes separating each header cell.
  The outer pipes (|) are optional, and you don't need to make the
  raw Markdown line up prettily. You can also use inline Markdown.

  Markdown | Less | Pretty
  --- | --- | ---
  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  1 | 2 | 3

  ---

  ## Blockquotes

  > Blockquotes are very handy in email to emulate reply text.
  > This line is part of the same quote.

  Quote break.

  > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.

  Nested quotes
  > Hello!
  >> And to you!

  ---

  ## Inline HTML

  ```html
  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
  ```

  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>

  ---

  ## Horizontal Rule

  Three or more...

  ---

  Hyphens (`-`)

  ***

  Asterisks (`*`)

  ___

  Underscores (`_`)


  ## License

  ```
    Copyright 2019 Dimitry Ivanov (legal@noties.io)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
  ```

  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet

  [arbitrary case-insensitive reference text]: https://www.mozilla.org
  [1]: http://slashdot.org
  [link text itself]: http://www.reddit.com
  
  [image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbkAAAChCAIAAADVxFY9AAAIb0lEQVR4Xu3aa4jVRRjH8R8JkiRFZWlXSZQVxRAkaUlcCivT7EahKIYiiEuLkiRdba0Mw1AMQ5CkJUmMytpuGok3FEOQpCVJkrQytRLDUAzD2Fh3znicoX1OL2bP+Tffz8t583/zzJeH4S8BACrRDgD4d7QSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBsVWul+zBQjfED/quqDesFdwV5W/B385b2LeGIALXEDWt4nN4FdwV563FWze3N4YgAtcQNa3icnr8nPXWml0731snLdOIKHb9Kv/XT0ev084368SYdGKj9ddo3RHuHqW249ozQ7pHaVa+do7S9QVvv0KY79cVYbRivT+/TRw/qg4f13kS9M1lrpmr1NLXM0KqZWtmoFU1aPkfL5mrJPC1+Soue1cLn9cILen6hnl2kpxZr3hLNXaY5y9W0Qo0rNXOVZrRo2mpNXaPJ72jie3r4Az34ke77VOM3aOwXunOT7tiqhu0atVP1uzRyt0bs0fA2DdurIftUt18DD+imH3Xjz7ruqPr9pquO64oTuuykep9WrzPqeVY92hsazl6kv3rq9MU61VsnLtXvl+tYH/3aV4ev0aEbdLC/vh+g7wZq32B9M0RtN+ur4do9Qrtu0Zf12n6btjVo8+3aOEaf36X14/TxvWp9QO8/pHcf0dpJWjNFb01Vy3S9MUMrZ2rFLL3epGWztXSuFj+hV57Uy0/rpflqbtb8l/TMy3ryFT2xWHOXavYyNb2uWSs0c6VmrNL0Fk19S1PWaOJaPfKuHnpfD7Tq3o81br3u+lxjNur2TWrYptu2q/5LjdylEbs1/Cvd3KYh32jwPg38TgO+V/8fdMMhXXNYfX/Vlcd0+e+69IQfAV1EK1Hz3LCGx+n5e5J1K3voTE/92UunLtEfpVb+0ldHrtWh6/VDZysH6ds67R2qtmHa09nKkdpZrx2jtHV0qZV367Nx+mSCWu/XulIr356i1Y+qZZpWdbayUcsf02tztPRxvdrZymf04nwtWKDnXiy18lU9vlRzXlPTcjWWWjmtRY+u1pS3Namzlet0/4ea8InGfaa7O1u5WaO3adQO3bqz1Mo9GtamoXs1+FsNKrXy+kO69oj6/qI+na38w48AeyUKwA1reJyevyeZt9Lvla6VV0Z75aDze6VrZdleuanUSr9XulZOjPbKxvN7pWtl2V75dKmVfq90rXwj2ivXnd8rXSvL9spbSq30e6Vr5cFor6SVKBQ3rOFxev6eZN7Kzr3yZPleeXW0Vw7u2Cu/Lt8rb+3YK7dFe+WH5Xvl5GivbOrYK5eU75XPdeyV86O98rHyvfLNaK9s7dgr7ynfK7d27JX10V5ZV75X/sReiQJzwxoep+fvSdatjN8r472yy/dKt1fG75XxXtnle6XbK+P3yniv7PK90u2V8XtlvFfyXolCccMaHqfn70nWrazwvfLcXhm/V8Z7pfFeeW6vjN8r473SeK88t1fG75XxXsl7Jf433LCGx+n5e5J5K3mvFK1EEbhhDY/T8/ck81byXilaiSJwwxoep+fvSdatjN8r+b8SqEluWMPj9Pw9ybqVFb5X8n8lUG1uWMPj9Pw9ybyVvFeKVqII3LCGx+n5e5J5K3mvFK1EEbhhDY/T8/ck61bG75XxXtnleyX/VwLdww1reJyevydZt7LC90r+rwSqzQ1reJyevyeZt5L3StFKFIEb1vA4PX9PMm8l75WilSgCN6zhcXr+nmTdyvi9kv8rgZrkhjU8Ts/fk6xbWeF7Jf9XAtXmhjU8Ts/fk8xbyXulaCWKwA1reJyevyeZt5L3StFKFIEb1vA4PX9Psm5l/F4Z75VdvlfyfyXQPdywhsfp+XuSdSsrfK/k/0qg2tywhsfp+XuSeSt5rxStRBG4YQ2P0/P3JPNW8l4pWokicMMaHqfn70nWrYzfK/m/EqhJbljD4/T8Pcm6lRW+V/J/JVBtbljD4/T8Pcm8lbxXilaiCNywhsfp+XuSeSt5rxStRBG4YQ2P0/P3JOtWxu+V8V7Z5Xsl/1cC3cMNa3icnr8nWbeywvdK/q8Eqs0Na3icnr8nmbeS90rRShSBG9bwOD1/TzJvJe+VopUoAjes4XF6/p5k3cr4vZL/K4Ga5IY1PE7P35OsW1nheyX/VwLV5oY1PE7P35PMW8l7pWglisANa3icnr8nmbeS90rRShSBG9bwOD1/T7JuZfxeGe+VXb5X8n8l0D3csIbH6Z2/KMgeeyVqnxvW8Di9C+4K8rbg7+Yt7VvCEQFqiRvW8Di9C+4K8hYOB1B7GFYAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALC5VgIAuvYP8v0NLroTl6oAAAAASUVORK5CYII=>
""".trimIndent()

================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/RichTextSample.kt
================================================
package com.zachklipp.richtext.sample

import androidx.annotation.IntRange
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.resolveDefaults

@Preview
@Composable private fun RichTextSamplePreview() {
  RichTextSample()
}

@Composable fun RichTextSample() {
  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }

  Column {
    // Config
    Card(elevation = CardDefaults.elevatedCardElevation()) {
      Column {
        RichTextStyleConfig(
          richTextStyle = richTextStyle,
          onChanged = { richTextStyle = it }
        )
      }
    }

    SelectionContainer {
      Column(Modifier.verticalScroll(rememberScrollState())) {
        RichTextDemo(style = richTextStyle)
      }
    }
  }
}

@Composable
fun RichTextStyleConfig(
  richTextStyle: RichTextStyle,
  onChanged: (RichTextStyle) -> Unit
) {
  Text("Paragraph spacing: ${richTextStyle.paragraphSpacing}")
  SliderForHumans(
    value = richTextStyle.paragraphSpacing!!.value,
    valueRange = 0f..20f,
    onValueChange = {
      onChanged(richTextStyle.copy(paragraphSpacing = it.sp))
    }
  )

  Text("Table cell padding: ${richTextStyle.tableStyle!!.cellPadding}")
  SliderForHumans(
    value = richTextStyle.tableStyle!!.cellPadding!!.value,
    valueRange = 0f..20f,
    onValueChange = {
      onChanged(
        richTextStyle.copy(
          tableStyle = richTextStyle.tableStyle!!.copy(
            cellPadding = it.sp
          )
        )
      )
    }
  )

  Text("Table border width padding: ${richTextStyle.tableStyle!!.borderStrokeWidth!!}")
  SliderForHumans(
    value = richTextStyle.tableStyle!!.borderStrokeWidth!!,
    valueRange = 0f..20f,
    onValueChange = {
      onChanged(
        richTextStyle.copy(
          tableStyle = richTextStyle.tableStyle!!.copy(
            borderStrokeWidth = it
          )
        )
      )
    }
  )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SliderForHumans(
  value: Float,
  onValueChange: (Float) -> Unit,
  modifier: Modifier = Modifier,
  enabled: Boolean = true,
  valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
  @IntRange(from = 0) steps: Int = 0,
  onValueChangeFinished: (() -> Unit)? = null,
  colors: SliderColors = SliderDefaults.colors(),
  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
  Slider(
    value = value,
    onValueChange = onValueChange,
    modifier = modifier,
    enabled = enabled,
    valueRange = valueRange,
    steps = steps,
    onValueChangeFinished = onValueChangeFinished,
    colors = colors,
    interactionSource = interactionSource,
    thumb = {
      SliderDefaults.Thumb(
        interactionSource = interactionSource,
        thumbSize = DpSize(4.dp, 20.dp)
      )
    }
  )
}

================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/SampleActivity.kt
================================================
package com.zachklipp.richtext.sample

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContent {
      SampleLauncher()
    }
  }
}


================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/SampleLauncher.kt
================================================
package com.zachklipp.richtext.sample

import androidx.activity.compose.BackHandler
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

private val Samples = listOf<Pair<String, @Composable () -> Unit>>(
  "RichText Demo" to @Composable { RichTextSample() },
  "Markdown Demo" to @Composable { MarkdownSample() },
  "Lazy Markdown Demo" to @Composable { LazyMarkdownSample() },
)

@Preview(showBackground = true)
@Composable private fun SampleLauncherPreview() {
  SamplesListScreen(isDarkTheme = true, onSampleClicked = {}, onThemeToggleClicked = {})
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable fun SampleLauncher() {
  val systemDarkTheme = isSystemInDarkTheme()
  var isDarkTheme by remember(systemDarkTheme) { mutableStateOf(systemDarkTheme) }
  var currentSampleIndex: Int? by remember { mutableStateOf(null) }

  SampleTheme(isDarkTheme) {
    Crossfade(currentSampleIndex) { index ->
      if (index != null) {
        BackHandler(onBack = { currentSampleIndex = null })
        Scaffold(
          topBar = {
            TopAppBar(title = { Text(Samples[index].first) }, actions = {
              val icon = if (isDarkTheme) Icons.Filled.LightMode else Icons.Filled.DarkMode
              IconButton(onClick = { isDarkTheme = !isDarkTheme }) {
                Icon(icon, contentDescription = "Change color scheme")
              }
            })
          }
        ) {
          Surface(Modifier.padding(it)) {
            Samples[index].second()
          }
        }
      } else {
        SamplesListScreen(
          isDarkTheme,
          onSampleClicked = { currentSampleIndex = it },
          onThemeToggleClicked = { isDarkTheme = !isDarkTheme }
        )
      }
    }
  }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable private fun SamplesListScreen(
  isDarkTheme: Boolean,
  onSampleClicked: (Int) -> Unit,
  onThemeToggleClicked: () -> Unit,
) {
  Scaffold(
    topBar = {
      TopAppBar(title = { Text("Samples") }, actions = {
        val icon = if (isDarkTheme) Icons.Filled.LightMode else Icons.Filled.DarkMode
        IconButton(onClick = onThemeToggleClicked) {
          Icon(icon, contentDescription = "Change color scheme")
        }
      })
    }
  ) { contentPadding ->
    LazyColumn(modifier = Modifier.padding(contentPadding)) {
      itemsIndexed(Samples) { index, (title, sampleContent) ->
        ListItem(
          headlineContent = { Text(title) },
          modifier = Modifier.clickable(onClick = { onSampleClicked(index) }),
          leadingContent = { SamplePreview(sampleContent) }
        )
      }
    }
  }
}

@Composable private fun SamplePreview(content: @Composable () -> Unit) {
  ScreenPreview(
    Modifier
      .size(50.dp)
      .aspectRatio(1f)
      .clipToBounds()
      // "Zoom in" to the top-start corner to make the preview more legible.
      .graphicsLayer(
        scaleX = 1.5f, scaleY = 1.5f,
        transformOrigin = TransformOrigin(0f, 0f)
      ),
  ) {
    SampleTheme {
      Surface(content = content)
    }
  }
}


================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/SampleTheme.kt
================================================
package com.zachklipp.richtext.sample

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.TextUnit

@Composable
fun SampleTheme(
  isDarkTheme: Boolean = isSystemInDarkTheme(),
  shapes: Shapes = MaterialTheme.shapes,
  typography: Typography = MaterialTheme.typography,
  content: @Composable () -> Unit
) {
  val supportsDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

  val lightColorScheme = lightColorScheme(primary = Color(0xFF1EB980))

  val darkColorScheme = darkColorScheme(primary = Color(0xFF66ffc7))

  val colorScheme =
    when {
      supportsDynamicColor && isDarkTheme -> {
        dynamicDarkColorScheme(LocalContext.current)
      }
      supportsDynamicColor && !isDarkTheme -> {
        dynamicLightColorScheme(LocalContext.current)
      }
      isDarkTheme -> darkColorScheme
      else -> lightColorScheme
    }
  MaterialTheme(colorScheme, shapes, typography) {
    val textStyle = LocalTextStyle.current.copy(lineHeight = TextUnit.Unspecified)
    CompositionLocalProvider(LocalTextStyle provides textStyle) {
      content()
    }
  }
}

================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/ScreenPreview.kt
================================================
@file:Suppress("DEPRECATION")

package com.zachklipp.richtext.sample

import android.content.Context
import android.content.Context.DISPLAY_SERVICE
import android.content.Context.WINDOW_SERVICE
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DisplayListener
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventPass.Initial
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize

/**
 * Displays [content] according to the current layout constraints, but with the density adjusted so
 * that the content things it's rendering inside a full-size screen, where "full-size" is defined
 * by [screenSize]. The default [screenSize] is read from the current window's default display.
 */
// TODO Disable focus
// TODO Disable key events (maybe covered by focus?)
// TODO use this for Slideshow as well.
@Composable fun ScreenPreview(
  modifier: Modifier = Modifier,
  screenSize: IntSize = rememberDefaultDisplaySize(),
  content: @Composable () -> Unit
) {
  val aspectRatio = screenSize.width.toFloat() / screenSize.height.toFloat()
  BoxWithConstraints(
    modifier
      .aspectRatio(aspectRatio)
      // Disable touch input.
      .then(PassthroughTouchToParentModifier)
      .semantics(mergeDescendants = true) {
        // TODO Block semantics. Is this enough?
        disabled()
      }
  ) {
    val actualDensity = LocalDensity.current.density
    // Can use width or height to do the calculation, since the aspect ratio is enforced.
    val previewDensityScale = constraints.maxWidth / screenSize.width.toFloat()
    val previewDensity = actualDensity * previewDensityScale

    // Provide a fake host view, since the preview doesn't really belong to this host view.
    val context = LocalContext.current
    val previewView = remember {
      val previewContext = context.applicationContext
      FrameLayout(previewContext)
    }

    DisableSelection {
      CompositionLocalProvider(
        LocalDensity provides Density(previewDensity),
        LocalView provides previewView,
        content = content
      )
    }
  }
}

/**
 * Returns the size of the default display for the window manager of the window this composable is
 * currently attached to. Will also recompose if the display size changes, e.g. when the device is
 * rotated.
 *
 * If the display reports an empty size (0x0), e.g. when running in a preview, then a reasonable
 * fake size of a phone display in portrait orientation is returned instead.
 */
@Composable private fun rememberDefaultDisplaySize(): IntSize {
  val context = LocalContext.current
  val state = remember { DisplaySizeCalculator(context) }
  return state.displaySize.value
}

private class DisplaySizeCalculator(context: Context) : RememberObserver,
  DisplayListener {
  private val windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
  private val displayManager = context.getSystemService(DISPLAY_SERVICE) as DisplayManager
  private val display = windowManager.defaultDisplay

  val displaySize = mutableStateOf(getDisplaySize())

  override fun onAbandoned() {
    // Noop
  }

  override fun onRemembered() {
    // Update the preview on device rotation, for example.
    displayManager.registerDisplayListener(this, Handler(Looper.getMainLooper()))
  }

  override fun onForgotten() {
    displayManager.unregisterDisplayListener(this)
  }

  override fun onDisplayChanged(displayId: Int) {
    if (displayId != display.displayId) return
    displaySize.value = getDisplaySize()
  }

  override fun onDisplayAdded(displayId: Int) = Unit
  override fun onDisplayRemoved(displayId: Int) = Unit

  private fun getDisplaySize(): IntSize {
    val metrics = DisplayMetrics().also(display::getMetrics)
    return if (metrics.widthPixels != 0 && metrics.heightPixels != 0) {
      IntSize(metrics.widthPixels, metrics.heightPixels)
    } else {
      // Zero-sized display? Probably in a preview. Return some fake reasonable default.
      IntSize(1080, 1920)
    }
  }
}

/**
 * A [PointerInputModifier] that blocks all touch events to children of the composable to which it's
 * applied, and instead allows all those events to flow to any filters defined on the parent
 * composable.
 */
private object PassthroughTouchToParentModifier : PointerInputModifier, PointerInputFilter() {
  override val pointerInputFilter: PointerInputFilter get() = this

  override fun onPointerEvent(
    pointerEvent: PointerEvent,
    pass: PointerEventPass,
    bounds: IntSize
  ) {
    if (pass == Initial) {
      // On the initial pass (ancestors -> descendants), mark all pointer events as completely
      // consumed. This prevents children from handling any pointer events.
      // These events are all marked as unconsumed by default.
      pointerEvent.changes.forEach {
        it.consumeAllChanges()
      }
    }
  }

  override fun onCancel() {
    // Noop.
  }
}


================================================
FILE: android-sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt
================================================
package com.zachklipp.richtext.sample

import android.content.Context
import android.widget.Toast
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap.Companion.Round
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.InlineContent
import com.halilibo.richtext.ui.string.RichTextString.Builder
import com.halilibo.richtext.ui.string.RichTextString.Format
import com.halilibo.richtext.ui.string.RichTextString.Format.Bold
import com.halilibo.richtext.ui.string.RichTextString.Format.Code
import com.halilibo.richtext.ui.string.RichTextString.Format.Italic
import com.halilibo.richtext.ui.string.RichTextString.Format.Link
import com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough
import com.halilibo.richtext.ui.string.RichTextString.Format.Subscript
import com.halilibo.richtext.ui.string.RichTextString.Format.Superscript
import com.halilibo.richtext.ui.string.RichTextString.Format.Underline
import com.halilibo.richtext.ui.string.Text
import com.halilibo.richtext.ui.string.richTextString
import com.halilibo.richtext.ui.string.withFormat
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Preview(showBackground = true)
@Composable fun TextPreview() {
  val context = LocalContext.current
  var toggleLink by remember { mutableStateOf(false) }
  val text = remember(context, toggleLink) {
    richTextString {
      appendPreviewSentence(Bold)
      appendPreviewSentence(Italic)
      appendPreviewSentence(Underline)
      appendPreviewSentence(Strikethrough)
      appendPreviewSentence(Subscript)
      appendPreviewSentence(Superscript)
      appendPreviewSentence(Code)
      appendPreviewSentence(
        Link("") { toggleLink = !toggleLink },
        if (toggleLink) "clicked link" else "link"
      )
      append("Here, ")
      appendInlineContent(content = spinningCross)
      append(", is an inline image. ")
      append("And here, ")
      appendInlineContent(content = slowLoadingImage)
      append(", is an inline image that loads after some delay.")
      append("\n\n")

      append("Here ")
      withFormat(Underline) {
        append("is a ")
        withFormat(Italic) {
          append("longer sentence ")
          withFormat(Bold) {
            append("with many ")
            withFormat(Code) {
              append("different ")
              withFormat(Strikethrough) {
                append("nested")
              }
              append(" ")
            }
          }
          append("styles.")
        }
      }
    }
  }
  RichText {
    Text(text)
  }
}

private val spinningCross = InlineContent {
  val angle = remember { Animatable(0f) }
  val color = remember { Animatable(Color.Red) }
  LaunchedEffect(Unit) {
    val angleAnim = infiniteRepeatable<Float>(
      animation = tween(durationMillis = 1000, easing = LinearEasing)
    )
    launch { angle.animateTo(360f, angleAnim) }

    val colorAnim = infiniteRepeatable<Color>(
      animation = keyframes {
        durationMillis = 2500
        Color.Blue at 500
        Color.Cyan at 1000
        Color.Green at 1500
        Color.Magenta at 2000
      }
    )
    launch { color.animateTo(Color.Yellow, colorAnim) }
  }

  Canvas(modifier = Modifier
    .size(12.sp.toDp(), 12.sp.toDp())
    .padding(2.dp)) {
    withTransform({ rotate(angle.value, center) }) {
      val strokeWidth = 3.dp.toPx()
      val strokeCap = Round
      drawLine(
        color.value,
        start = Offset(0f, size.height / 2),
        end = Offset(size.width, size.height / 2),
        strokeWidth = strokeWidth,
        cap = strokeCap
      )
      drawLine(
        color.value,
        start = Offset(size.width / 2, 0f),
        end = Offset(size.width / 2, size.height),
        strokeWidth = strokeWidth,
        cap = strokeCap
      )
    }
  }
}

val slowLoadingImage = InlineContent {
  var loaded by rememberSaveable { mutableStateOf(false) }
  LaunchedEffect(loaded) {
    if (!loaded) {
      delay(3000)
      loaded = true
    }
  }

  if (!loaded) {
    LoadingSpinner()
  } else {
    Box(Modifier.clickable(onClick = { loaded = false })) {
      val size = remember { Animatable(16f) }
      LaunchedEffect(Unit) { size.animateTo(100f) }
      Picture(Modifier.size(size.value.sp.toDp()))
      Text(
        "click to refresh",
        modifier = Modifier
          .padding(3.dp)
          .align(Alignment.Center),
        fontSize = 8.sp,
        style = TextStyle(background = Color.LightGray)
      )
    }
  }
}

@Composable private fun LoadingSpinner() {
  val alpha = remember { Animatable(1f) }
  LaunchedEffect(Unit) {
    val anim = infiniteRepeatable<Float>(
      animation = keyframes {
        durationMillis = 500
        0f at 250
        1f at 500
      })
    alpha.animateTo(0f, anim)
  }
  Text(
    "⏳",
    fontSize = 3.em,
    modifier = Modifier
      .wrapContentSize(Alignment.Center)
      .graphicsLayer(alpha = alpha.value)
  )
}

@Composable private fun Picture(modifier: Modifier) {
  Canvas(modifier) {
    drawRect(Color.LightGray)
    drawLine(Color.Red, Offset(0f, 0f), Offset(size.width, size.height))
    drawLine(Color.Red, Offset(0f, size.height), Offset(size.width, 0f))
  }
}

@OptIn(ExperimentalStdlibApi::class)
private fun Builder.appendPreviewSentence(
  format: Format,
  text: String = format.javaClass.simpleName.replaceFirstChar { it.lowercase() }
) {
  append("Here is some ")
  withFormat(format) {
    append(text)
  }
  append(" text. ")
}

@Composable
fun ProvideToastUriHandler(context: Context, content: @Composable () -> Unit) {
  val uriHandler = remember(context) {
    object : UriHandler {
      override fun openUri(uri: String) {
        Toast.makeText(context, uri, Toast.LENGTH_SHORT).show()
      }
    }
  }

  CompositionLocalProvider(LocalUriHandler provides uriHandler, content)
}


================================================
FILE: android-sample/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
  <path
      android:fillColor="#3DDC84"
      android:pathData="M0,0h108v108h-108z" />
  <path
      android:fillColor="#00000000"
      android:pathData="M9,0L9,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,0L19,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M29,0L29,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M39,0L39,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M49,0L49,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M59,0L59,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M69,0L69,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M79,0L79,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M89,0L89,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M99,0L99,108"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,9L108,9"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,19L108,19"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,29L108,29"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,39L108,39"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,49L108,49"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,59L108,59"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,69L108,69"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,79L108,79"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,89L108,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M0,99L108,99"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,29L89,29"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,39L89,39"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,49L89,49"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,59L89,59"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,69L89,69"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M19,79L89,79"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M29,19L29,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M39,19L39,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M49,19L49,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M59,19L59,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M69,19L69,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
  <path
      android:fillColor="#00000000"
      android:pathData="M79,19L79,89"
      android:strokeWidth="0.8"
      android:strokeColor="#33FFFFFF" />
</vector>


================================================
FILE: android-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
  <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
    <aapt:attr name="android:fillColor">
      <gradient
          android:endX="85.84757"
          android:endY="92.4963"
          android:startX="42.9492"
          android:startY="49.59793"
          android:type="linear">
        <item
            android:color="#44000000"
            android:offset="0.0" />
        <item
            android:color="#00000000"
            android:offset="1.0" />
      </gradient>
    </aapt:attr>
  </path>
  <path
      android:fillColor="#FFFFFF"
      android:fillType="nonZero"
      android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
      android:strokeWidth="1"
      android:strokeColor="#00000000" />
</vector>

================================================
FILE: android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  <background android:drawable="@drawable/ic_launcher_background" />
  <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  <background android:drawable="@drawable/ic_launcher_background" />
  <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: android-sample/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#eeeeee</color>
  <color name="colorPrimaryDark">#111111</color>
  <color name="colorAccent">#2079c7</color>
</resources>


================================================
FILE: android-sample/src/main/res/values/strings.xml
================================================
<resources>
  <string name="app_name">Rich Text Sample</string>
</resources>

================================================
FILE: android-sample/src/main/res/values/styles.xml
================================================
<resources>
  <!-- Base application theme. -->
  <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>
</resources>


================================================
FILE: build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
  id("org.jetbrains.dokka")
}

repositories {
  mavenCentral()
}

dokka {
  dokkaPublications.configureEach {
    outputDirectory.set(rootProject.file("docs/api"))
  }
}

dependencies {
  dokka(project(":richtext-ui"))
  dokka(project(":richtext-ui-material"))
  dokka(project(":richtext-ui-material3"))
  dokka(project(":richtext-markdown"))
  dokka(project(":richtext-commonmark"))
}

// See https://stackoverflow.com/questions/25324880/detect-ide-environment-with-gradle
fun isRunningFromIde(): Boolean {
  return project.properties["android.injected.invoked.from.ide"] == "true"
}

subprojects {
  repositories {
    google()
    mavenCentral()
  }

  tasks.withType<KotlinCompile>().all {
    compilerOptions {
//       TODO(stable); Disable warnings as errors until we get to 1.0.0
//       Allow warnings when running from IDE, makes it easier to experiment.
//      if (!isRunningFromIde()) {
//        allWarningsAsErrors = true
//      }

      freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xexpect-actual-classes")
    }
  }

  // taken from https://github.com/google/accompanist/blob/main/build.gradle
  afterEvaluate {
    if (tasks.findByName("dokkaHtmlPartial") == null) {
      // If dokka isn't enabled on this module, skip
      return@afterEvaluate
    }
  }
}

//disable until the library reaches 1.0.0-beta01
//apply plugin: 'binary-compatibility-validator'
//apiValidation {
//  // Ignore all sample projects, since they're not part of our API.
//  // Only leaf project name is valid configuration, and every project must be individually ignored.
//  // See https://github.com/Kotlin/binary-compatibility-validator/issues/3
//  ignoredProjects += project('sample').name
//  ignoredProjects += project('desktop').name
//  ignoredProjects += project('richtext-ui-kmm').name
//  ignoredProjects += project('richtext-commonmark-kmm').name
//}


================================================
FILE: buildSrc/build.gradle.kts
================================================
repositories {
  google()
  mavenCentral()
}

plugins {
  `kotlin-dsl`
  `kotlin-dsl-precompiled-script-plugins`
}

dependencies {
  // keep in sync with Dependencies.BuildPlugins.androidGradlePlugin
  implementation("com.android.tools.build:gradle:9.1.0")
  implementation("com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.36.0")
  // keep in sync with Dependencies.Kotlin.gradlePlugin
  implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
  // keep in sync with Dependencies.Compose.desktopVersion
  implementation("org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:1.11.0-beta01")
  // keep in sync with Dependencies.Kotlin.version
  implementation("org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10")
  implementation("org.jetbrains.dokka:dokka-gradle-plugin:2.2.0")
  implementation(kotlin("script-runtime"))
}

================================================
FILE: buildSrc/src/main/kotlin/Dependencies.kt
================================================
object BuildPlugins {
  // keep in sync with buildSrc/build.gradle.kts
  val androidGradlePlugin = "com.android.tools.build:gradle:9.1.0"
}

object AndroidX {
  val appcompat = "androidx.appcompat:appcompat:1.7.1"
}

object Network {
  val okHttp = "com.squareup.okhttp3:okhttp:4.9.0"
}

object Kotlin {
  // keep in sync with buildSrc/build.gradle.kts
  val version = "2.3.10"
  val binaryCompatibilityValidatorPlugin = "org.jetbrains.kotlinx:binary-compatibility-validator:0.9.0"
  val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version"

  val composeCompilerPlugin = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:$version"

  object Test {
    val common = "org.jetbrains.kotlin:kotlin-test-common"
    val annotations = "org.jetbrains.kotlin:kotlin-test-annotations-common"
    val jdk = "org.jetbrains.kotlin:kotlin-test-junit"
  }
}

val ktlint = "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"

object Compose {
  val desktopVersion = "1.11.0-beta01"

  val jetbrainsComposePlugin = "org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:$desktopVersion"
  val activity = "androidx.activity:activity-compose:1.8.2"
  val toolingData = "androidx.compose.ui:ui-tooling-data:1.6.0"
  val coil = "io.coil-kt.coil3:coil-compose:3.3.0"
  val coilHttp = "io.coil-kt.coil3:coil-network-okhttp:3.3.0"
}

object Commonmark {
  private val version = "0.26.0"
  val core = "org.commonmark:commonmark:$version"
  val tables = "org.commonmark:commonmark-ext-gfm-tables:$version"
  val strikethrough = "org.commonmark:commonmark-ext-gfm-strikethrough:$version"
  val autolink = "org.commonmark:commonmark-ext-autolink:$version"
}

object AndroidConfiguration {
  val minSdk = 23
  val targetSdk = 36
  val compileSdk = targetSdk
}


================================================
FILE: buildSrc/src/main/kotlin/richtext-kmp-library.gradle.kts
================================================
import AndroidConfiguration.compileSdk
import AndroidConfiguration.minSdk
import AndroidConfiguration.targetSdk
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
  kotlin("multiplatform")
  id("com.android.kotlin.multiplatform.library")
  id("org.jetbrains.kotlin.plugin.compose")
  id("org.jetbrains.compose")
  id("com.vanniktech.maven.publish")
  id("org.jetbrains.dokka")
  signing
}

repositories {
  google()
  mavenCentral()
}

signing {
  val signingKey = System.getenv("GPG_PRIVATE_KEY")?.replace("\\n", "\n")
  val signingPassword = System.getenv("GPG_PRIVATE_PASSWORD")
  if (signingKey != null && signingPassword != null) {
    useInMemoryPgpKeys(signingKey, signingPassword)
  }
}

// Maven Central credentials are provided via ORG_GRADLE_PROJECT_mavenCentralUsername
// and ORG_GRADLE_PROJECT_mavenCentralPassword environment variables.
mavenPublishing {
  publishToMavenCentral()
  signAllPublications()
}

kotlin {
  jvm()
  explicitApi()

  android {
    compileSdk = 36

    compilerOptions {
      jvmTarget.set(JvmTarget.JVM_11)
    }
  }
}



================================================
FILE: desktop-sample/build.gradle.kts
================================================
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
  kotlin("jvm")
  id("org.jetbrains.compose")
  id("org.jetbrains.kotlin.plugin.compose")
}

dependencies {
  implementation(project(":richtext-commonmark"))
  implementation(project(":richtext-ui-material"))
  implementation(compose.desktop.currentOs)
  implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3")
}

compose.desktop {
  application {
    mainClass = "com.halilibo.richtext.desktop.MarkdownSampleAppKt"
    nativeDistributions {
      targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
      packageName = "jvm"
      packageVersion = "1.0.0"
    }
  }
}

================================================
FILE: desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/MarkdownSampleApp.kt
================================================
package com.halilibo.richtext.desktop

import androidx.compose.foundation.LocalScrollbarStyle
import androidx.compose.foundation.background
import androidx.compose.foundation.defaultScrollbarStyle
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.LeadingIconTab
import androidx.compose.material.Slider
import androidx.compose.material.Surface
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.Markdown
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.markdown.node.AstDocument
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.ui.CodeBlockStyle
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.currentRichTextStyle
import com.halilibo.richtext.ui.material.RichText
import com.halilibo.richtext.ui.resolveDefaults

fun main(): Unit = singleWindowApplication(
  title = "RichText KMP"
) {
  var richTextStyle by remember {
    mutableStateOf(
      RichTextStyle(
        codeBlockStyle = CodeBlockStyle(wordWrap = true)
      ).resolveDefaults()
    )
  }

  Surface {
    CompositionLocalProvider(
      LocalScrollbarStyle provides defaultScrollbarStyle().copy(
        hoverColor = Color.DarkGray,
        unhoverColor = Color.Gray
      )
    ) {
      SelectionContainer {
        val state = rememberTextFieldState(sampleMarkdown)
        Row(
          modifier = Modifier
            .padding(32.dp)
            .fillMaxSize(),
          horizontalArrangement = Arrangement.spacedBy(32.dp)
        ) {
          Column(modifier = Modifier.weight(1f)) {
            DisableSelection {
              RichTextStyleConfig(richTextStyle = richTextStyle, onChanged = { richTextStyle = it })
            }
            BasicTextField(
              state = state,
              modifier = Modifier
                .fillMaxHeight()
                .background(Color.LightGray)
                .padding(8.dp)
            )
          }
          var selectedTab by remember { mutableStateOf(0) }
          Column(Modifier.weight(1f)) {
            DisableSelection {
              TabRow(selectedTab) {
                LeadingIconTab(
                  selected = selectedTab == 0,
                  onClick = { selectedTab = 0 },
                  text = { Text("Normal") },
                  icon = { Icon(Icons.Default.Info, "") })
                LeadingIconTab(
                  selected = selectedTab == 1,
                  onClick = { selectedTab = 1 },
                  text = { Text("Lazy") },
                  icon = { Icon(Icons.Default.Favorite, "") })
              }
            }
            ProvidePrintUriHandler {
              if (selectedTab == 0) {
                RichText(
                  modifier = Modifier.verticalScroll(rememberScrollState()),
                  style = richTextStyle,
                ) {
                  Markdown(content = state.text.toString())
                }
              } else {
                val parser = remember { CommonmarkAstNodeParser() }

                val astNode = remember(parser) {
                  parser.parse(sampleMarkdown)
                }

                RichText(
                  style = richTextStyle,
                ) {
                  LazyMarkdown(astNode)
                }
              }
            }
          }
        }
      }
    }
  }
}

/**
 * A function that renders Markdown content lazily at the top level. All markdown trees start with
 * an AstDocument. If a document is long enough, usually there are more than hundred child nodes
 * under the root. Then in turn rendering the whole content into the internal column of
 * `BasicRichText` becomes extremely inefficient. Instead, this renderer at least relieves the top
 * level rendering by turning the internal column into a LazyColumn. All other nodes below the
 * first level are rendered as usual.
 *
 * @param astNode Root node of Markdown tree. This can be obtained via a parser.
 */
@Composable
fun RichTextScope.LazyMarkdown(astNode: AstNode) {
  require(astNode.type == AstDocument) {
    "Lazy Markdown rendering requires root level node to have a type of AstDocument."
  }
  // keep the same blockSpacing
  val currentStyle = currentRichTextStyle
  val resolvedStyle = remember(currentStyle) { currentStyle.resolveDefaults() }
  val blockSpacing = with(LocalDensity.current) {
    resolvedStyle.paragraphSpacing!!.toDp()
  }
  LazyColumn(verticalArrangement = Arrangement.spacedBy(blockSpacing)) {
    var iter = astNode.links.firstChild
    while (iter != null) {
      // We need to store iter in a final variable because composition of `item` happens after
      // iteration
      val node = iter
      item {
        BasicMarkdown(node)
      }
      iter = iter.links.next
    }
  }
}

@Composable
fun RichTextStyleConfig(
  richTextStyle: RichTextStyle,
  onChanged: (RichTextStyle) -> Unit
) {
  Column(modifier = Modifier.fillMaxWidth()) {
    Row {
      Column(Modifier.weight(1f)) {
        Text("Paragraph spacing:\n${richTextStyle.paragraphSpacing}")
        Slider(
          value = richTextStyle.paragraphSpacing!!.value,
          valueRange = 0f..20f,
          onValueChange = {
            onChanged(richTextStyle.copy(paragraphSpacing = it.sp))
          }
        )
      }
      Column(Modifier.weight(1f)) {
        Text("List item spacing:\n${richTextStyle.listStyle!!.itemSpacing}")
        Slider(
          value = richTextStyle.listStyle!!.itemSpacing!!.value,
          valueRange = 0f..20f,
          onValueChange = {
            onChanged(
              richTextStyle.copy(
                listStyle = richTextStyle.listStyle!!.copy(
                  itemSpacing = it.sp
                )
              )
            )
          }
        )
      }
    }
    Row {
      Column(Modifier.weight(1f)) {
        Text("Table cell padding:\n${richTextStyle.tableStyle!!.cellPadding}")
        Slider(
          value = richTextStyle.tableStyle!!.cellPadding!!.value,
          valueRange = 0f..20f,
          onValueChange = {
            onChanged(
              richTextStyle.copy(
                tableStyle = richTextStyle.tableStyle!!.copy(
                  cellPadding = it.sp
                )
              )
            )
          }
        )
      }
      Column(Modifier.weight(1f)) {
        Text("Table border width padding:\n${richTextStyle.tableStyle!!.borderStrokeWidth!!}")
        Slider(
          value = richTextStyle.tableStyle!!.borderStrokeWidth!!,
          valueRange = 0f..20f,
          onValueChange = {
            onChanged(
              richTextStyle.copy(
                tableStyle = richTextStyle.tableStyle!!.copy(
                  borderStrokeWidth = it
                )
              )
            )
          }
        )
      }
    }
  }
}

@Composable
fun ProvidePrintUriHandler(content: @Composable () -> Unit) {
  val uriHandler = remember {
    object : UriHandler {
      override fun openUri(uri: String) {
        println("Link clicked destination=$uri")
      }
    }
  }

  CompositionLocalProvider(LocalUriHandler provides uriHandler, content)
}

private val sampleMarkdown = """
  # Demo
  Based on [this cheatsheet][cheatsheet]

  ---

  ## Headers
  ---
  # Header 1
  ## Header 2
  ### Header 3
  #### Header 4
  ##### Header 5
  ###### Header 6
  ---
  
  ## Full-bleed Image
  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)

  ## Images smaller than the width should center
  ![](https://cdn.nostr.build/p/4a84.png)
  
  On LineHeight bug, the image below goes over this text. 
  ![](https://cdn.nostr.build/p/PxZ0.jpg)

  ## Emphasis

  Emphasis, aka italics, with *asterisks* or _underscores_.

  Strong emphasis, aka bold, with **asterisks** or __underscores__.

  Combined emphasis with **asterisks and _underscores_**.

  ---

  ## Lists
  1. First ordered list item
  2. Another item
      * Unordered sub-list.
  1. Actual numbers don't matter, just that it's a number
      1. Ordered sub-list
  4. And another item.

      You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).

      To have a line break without a paragraph, you will need to use two trailing spaces.
      Note that this line is separate, but within the same paragraph.
      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)

  * Unordered list can use asterisks
  - Or minuses
  + Or pluses
<!-- -->
  2. Ordered list starting with `2.`
  3. Another item
<!-- -->
  0. Ordered list starting with `0.`
<!-- -->
  003. Ordered list starting with `003.`
<!-- -->
  -1. Starting with `-1.` should not be list


  ---

  ## Links

  [I'm an inline-style link](https://www.google.com)

  [I'm a reference-style link][Arbitrary case-insensitive reference text]

  [I'm a relative reference to a repository file](../blob/master/LICENSE)

  [You can use numbers for reference-style link definitions][1]

  Or leave it empty and use the [link text itself].
  
  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.

  ---

  ## Code

  Inline `code` has `back-ticks around` it.

  ```javascript
  var s = "JavaScript syntax highlighting";
  alert(s);
  ```

  ```python
  s = "Python syntax highlighting"
  print s
  ```

  ```java
  /**
   * Helper method to obtain a Parser with registered strike-through &amp; table extensions
   * &amp; task lists (added in 1.0.1)
   *
   * @return a Parser instance that is supported by this library
   * @since 1.0.0
   */
  @NonNull
  public static Parser createParser() {
    return new Parser.Builder()
        .extensions(Arrays.asList(
            StrikethroughExtension.create(),
            TablesExtension.create(),
            TaskListExtension.create()
        ))
        .build();
  }
  ```

  ```xml
  <ScrollView
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="?android:attr/actionBarSize">

    <TextView
      android:id="@+id/text"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="16dip"
      android:lineSpacingExtra="2dip"
      android:textSize="16sp"
      tools:text="yo\nman" />

  </ScrollView>
  ```

  ```
  No language indicated, so no syntax highlighting.
  But let's throw in a <b>tag</b>.
  ```
  
  ---

  ## Images
  
  Inline-style:
   
  ![random image](https://picsum.photos/seed/picsum/400/400)
  
  ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  
  Reference-style:
   
  ![random image][logo]
  
  [logo]: https://picsum.photos/seed/picsum2/400/400 "Text 2"
  
  Base64 Inline
  
  ![][image1]

  ---

  ## Tables

  Colons can be used to align columns.

  | Tables        | Are           | Cool  |
  | ------------- |:-------------:| -----:|
  | col 3 is      | right-aligned | ${'$'}1600 |
  | col 2 is      | centered      |   ${'$'}12 |
  | zebra stripes | are neat      |    ${'$'}1 |

  There must be at least 3 dashes separating each header cell.
  The outer pipes (|) are optional, and you don't need to make the
  raw Markdown line up prettily. You can also use inline Markdown.

  Markdown | Less | Pretty
  --- | --- | ---
  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
  1 | 2 | 3

  ---

  ## Blockquotes

  > Blockquotes are very handy in email to emulate reply text.
  > This line is part of the same quote.

  Quote break.

  > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.

  Nested quotes
  > Hello!
  >> And to you!

  ---

  ## Inline HTML

  ```html
  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
  ```

  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>

  ---

  ## Horizontal Rule

  Three or more...

  ---

  Hyphens (`-`)

  ***

  Asterisks (`*`)

  ___

  Underscores (`_`)


  ## License

  ```
    Copyright 2019 Dimitry Ivanov (legal@noties.io)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
  ```

  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet

  [arbitrary case-insensitive reference text]: https://www.mozilla.org
  [1]: http://slashdot.org
  [link text itself]: http://www.reddit.com
  
  [image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbkAAAChCAIAAADVxFY9AAAIb0lEQVR4Xu3aa4jVRRjH8R8JkiRFZWlXSZQVxRAkaUlcCivT7EahKIYiiEuLkiRdba0Mw1AMQ5CkJUmMytpuGok3FEOQpCVJkrQytRLDUAzD2Fh3znicoX1OL2bP+Tffz8t583/zzJeH4S8BACrRDgD4d7QSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBstBIAbLQSAGy0EgBsVWul+zBQjfED/quqDesFdwV5W/B385b2LeGIALXEDWt4nN4FdwV563FWze3N4YgAtcQNa3icnr8nPXWml0731snLdOIKHb9Kv/XT0ev084368SYdGKj9ddo3RHuHqW249ozQ7pHaVa+do7S9QVvv0KY79cVYbRivT+/TRw/qg4f13kS9M1lrpmr1NLXM0KqZWtmoFU1aPkfL5mrJPC1+Soue1cLn9cILen6hnl2kpxZr3hLNXaY5y9W0Qo0rNXOVZrRo2mpNXaPJ72jie3r4Az34ke77VOM3aOwXunOT7tiqhu0atVP1uzRyt0bs0fA2DdurIftUt18DD+imH3Xjz7ruqPr9pquO64oTuuykep9WrzPqeVY92hsazl6kv3rq9MU61VsnLtXvl+tYH/3aV4ev0aEbdLC/vh+g7wZq32B9M0RtN+ur4do9Qrtu0Zf12n6btjVo8+3aOEaf36X14/TxvWp9QO8/pHcf0dpJWjNFb01Vy3S9MUMrZ2rFLL3epGWztXSuFj+hV57Uy0/rpflqbtb8l/TMy3ryFT2xWHOXavYyNb2uWSs0c6VmrNL0Fk19S1PWaOJaPfKuHnpfD7Tq3o81br3u+lxjNur2TWrYptu2q/5LjdylEbs1/Cvd3KYh32jwPg38TgO+V/8fdMMhXXNYfX/Vlcd0+e+69IQfAV1EK1Hz3LCGx+n5e5J1K3voTE/92UunLtEfpVb+0ldHrtWh6/VDZysH6ds67R2qtmHa09nKkdpZrx2jtHV0qZV367Nx+mSCWu/XulIr356i1Y+qZZpWdbayUcsf02tztPRxvdrZymf04nwtWKDnXiy18lU9vlRzXlPTcjWWWjmtRY+u1pS3Namzlet0/4ea8InGfaa7O1u5WaO3adQO3bqz1Mo9GtamoXs1+FsNKrXy+kO69oj6/qI+na38w48AeyUKwA1reJyevyeZt9Lvla6VV0Z75aDze6VrZdleuanUSr9XulZOjPbKxvN7pWtl2V75dKmVfq90rXwj2ivXnd8rXSvL9spbSq30e6Vr5cFor6SVKBQ3rOFxev6eZN7Kzr3yZPleeXW0Vw7u2Cu/Lt8rb+3YK7dFe+WH5Xvl5GivbOrYK5eU75XPdeyV86O98rHyvfLNaK9s7dgr7ynfK7d27JX10V5ZV75X/sReiQJzwxoep+fvSdatjN8r472yy/dKt1fG75XxXtnle6XbK+P3yniv7PK90u2V8XtlvFfyXolCccMaHqfn70nWrazwvfLcXhm/V8Z7pfFeeW6vjN8r473SeK88t1fG75XxXsl7Jf433LCGx+n5e5J5K3mvFK1EEbhhDY/T8/ck81byXilaiSJwwxoep+fvSdatjN8r+b8SqEluWMPj9Pw9ybqVFb5X8n8lUG1uWMPj9Pw9ybyVvFeKVqII3LCGx+n5e5J5K3mvFK1EEbhhDY/T8/ck61bG75XxXtnleyX/VwLdww1reJyevydZt7LC90r+rwSqzQ1reJyevyeZt5L3StFKFIEb1vA4PX9PMm8l75WilSgCN6zhcXr+nmTdyvi9kv8rgZrkhjU8Ts/fk6xbWeF7Jf9XAtXmhjU8Ts/fk8xbyXulaCWKwA1reJyevyeZt5L3StFKFIEb1vA4PX9Psm5l/F4Z75VdvlfyfyXQPdywhsfp+XuSdSsrfK/k/0qg2tywhsfp+XuSeSt5rxStRBG4YQ2P0/P3JPNW8l4pWokicMMaHqfn70nWrYzfK/m/EqhJbljD4/T8Pcm6lRW+V/J/JVBtbljD4/T8Pcm8lbxXilaiCNywhsfp+XuSeSt5rxStRBG4YQ2P0/P3JOtWxu+V8V7Z5Xsl/1cC3cMNa3icnr8nWbeywvdK/q8Eqs0Na3icnr8nmbeS90rRShSBG9bwOD1/TzJvJe+VopUoAjes4XF6/p5k3cr4vZL/K4Ga5IY1PE7P35OsW1nheyX/VwLV5oY1PE7P35PMW8l7pWglisANa3icnr8nmbeS90rRShSBG9bwOD1/T7JuZfxeGe+VXb5X8n8l0D3csIbH6Z2/KMgeeyVqnxvW8Di9C+4K8rbg7+Yt7VvCEQFqiRvW8Di9C+4K8hYOB1B7GFYAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALDRSgCw0UoAsNFKALC5VgIAuvYP8v0NLroTl6oAAAAASUVORK5CYII=>
""".trimIndent()

================================================
FILE: desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/RichTextSampleApp.kt
================================================
package com.halilibo.richtext.desktop

import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalScrollbarStyle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.defaultScrollbarStyle
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
import com.halilibo.richtext.ui.BlockQuote
import com.halilibo.richtext.ui.CodeBlock
import com.halilibo.richtext.ui.CodeBlockStyle
import com.halilibo.richtext.ui.FormattedList
import com.halilibo.richtext.ui.Heading
import com.halilibo.richtext.ui.HorizontalRule
import com.halilibo.richtext.ui.InfoPanel
import com.halilibo.richtext.ui.InfoPanelType
import com.halilibo.richtext.ui.ListType
import com.halilibo.richtext.ui.ListType.Ordered
import com.halilibo.richtext.ui.ListType.Unordered
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.Table
import com.halilibo.richtext.ui.material.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.halilibo.richtext.ui.string.InlineContent
import com.halilibo.richtext.ui.string.RichTextString.Builder
import com.halilibo.richtext.ui.string.RichTextString.Format
import com.halilibo.richtext.ui.string.RichTextString.Format.Bold
import com.halilibo.richtext.ui.string.RichTextString.Format.Code
import com.halilibo.richtext.ui.string.RichTextString.Format.Italic
import com.halilibo.richtext.ui.string.RichTextString.Format.Link
import com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough
import com.halilibo.richtext.ui.string.RichTextString.Format.Subscript
import com.halilibo.richtext.ui.string.RichTextString.Format.Superscript
import com.halilibo.richtext.ui.string.RichTextString.Format.Underline
import com.halilibo.richtext.ui.string.Text
import com.halilibo.richtext.ui.string.richTextString
import com.halilibo.richtext.ui.string.withFormat
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

fun main(): Unit = singleWindowApplication(
  title = "RichText KMP"
) {
  var richTextStyle by remember {
    mutableStateOf(
      RichTextStyle(
        codeBlockStyle = CodeBlockStyle(wordWrap = true)
      ).resolveDefaults()
    )
  }

  Surface {
    CompositionLocalProvider(
      LocalScrollbarStyle provides defaultScrollbarStyle().copy(
        hoverColor = Color.DarkGray,
        unhoverColor = Color.Gray
      )
    ) {
      SelectionContainer {
        Row(
          modifier = Modifier
            .padding(32.dp)
            .fillMaxSize(),
          horizontalArrangement = Arrangement.spacedBy(32.dp)
        ) {
          Column(modifier = Modifier.weight(1f)) {
            RichTextStyleConfig(richTextStyle = richTextStyle, onChanged = { richTextStyle = it })
          }
          Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
            RichTextDemo(style = richTextStyle)
          }
        }
      }
    }
  }
}

@Composable fun RichTextDemo(
  style: RichTextStyle? = null,
  header: String = ""
) {
  RichText(
    modifier = Modifier.padding(8.dp),
    style = style
  ) {
    Heading(0, "Paragraphs $header")
    Text("Simple paragraph.")
    Text("Paragraph with\nmultiple lines.")
    Text("Paragraph with really long line that should be getting wrapped.")
    TextPreview()

    Heading(0, "Lists")
    Heading(1, "Unordered")
    ListDemo(listType = Unordered)
    Heading(1, "Ordered")
    ListDemo(listType = Ordered)

    Heading(0, "Horizontal Line")
    Text("Above line")
    HorizontalRule()
    Text("Below line")

    Heading(0, "Code Block")
    CodeBlock(
      """
        {
          "Hello": "world!"
        }
      """.trimIndent()
    )

    Heading(0, "Block Quote")
    BlockQuote {
      Text("These paragraphs are quoted.")
      Text("More text.")
      BlockQuote {
        Text("Nested block quote.")
      }
    }

    Heading(0, "Info Panel")
    InfoPanel(InfoPanelType.Primary, "Only text primary info panel")
    InfoPanel(InfoPanelType.Success) {
      Column {
        Text("Successfully sent some data")
        HorizontalRule()
        BlockQuote {
          Text("This is a quote")
        }
      }
    }

    Heading(0, "Table")
    Table(
      modifier = Modifier.fillMaxWidth(),
      headerRow = {
        cell { Text("Column 1") }
        cell { Text("Column 2") }
      }) {
      row {
        cell { Text("Hello") }
        cell {
          CodeBlock("Foo bar")
        }
      }
      row {
        cell {
          BlockQuote {
            Text("Stuff")
          }
        }
        cell { Text("Hello world this is a really long line that is going to wrap hopefully") }
      }
    }
  }
}

@Composable private fun RichTextScope.ListDemo(listType: ListType) {
  FormattedList(listType,
    @Composable {
      Text("First list item")
      FormattedList(listType,
        @Composable { Text("Indented 1") }
      )
    },
    @Composable {
      Text("")
    },
    @Composable {
      Text("hello")
    },
    @Composable {
      Text("Second list item.")
      FormattedList(listType,
        @Composable { Text("Indented 2") }
      )
    }
  )
}

@Composable fun TextPreview() {
  var toggleLink by remember { mutableStateOf(false) }
  val text = remember(toggleLink) {
    richTextString {
      appendPreviewSentence(Bold)
      appendPreviewSentence(Italic)
      appendPreviewSentence(Underline)
      appendPreviewSentence(Strikethrough)
      appendPreviewSentence(Subscript)
      appendPreviewSentence(Superscript)
      appendPreviewSentence(Code)
      appendPreviewSentence(
        Link("") { toggleLink = !toggleLink },
        if (toggleLink) "clicked link" else "link"
      )
      append("Here, ")
      appendInlineContent(content = spinningCross)
      append(", is an inline image. ")
      append("And here, ")
      appendInlineContent(content = slowLoadingImage)
      append(", is an inline image that loads after some delay.")
      append("\n\n")

      append("Here ")
      withFormat(Underline) {
        append("is a ")
        withFormat(Italic) {
          append("longer sentence ")
          withFormat(Bold) {
            append("with many ")
            withFormat(Code) {
              append("different ")
              withFormat(Strikethrough) {
                append("nested")
              }
              append(" ")
            }
          }
          append("styles.")
        }
      }
    }
  }
  RichText {
    Text(text)
  }
}

private val spinningCross = InlineContent {
  val angle = remember { Animatable(0f) }
  val color = remember { Animatable(Color.Red) }
  LaunchedEffect(Unit) {
    val angleAnim = infiniteRepeatable<Float>(
      animation = tween(durationMillis = 1000, easing = LinearEasing)
    )
    launch { angle.animateTo(360f, angleAnim) }

    val colorAnim = infiniteRepeatable<Color>(
      animation = keyframes {
        durationMillis = 2500
        Color.Blue at 500
        Color.Cyan at 1000
        Color.Green at 1500
        Color.Magenta at 2000
      }
    )
    launch { color.animateTo(Color.Yellow, colorAnim) }
  }

  Canvas(modifier = Modifier
    .size(12.sp.toDp(), 12.sp.toDp())
    .padding(2.dp)) {
    withTransform({ rotate(angle.value, center) }) {
      val strokeWidth = 3.dp.toPx()
      val strokeCap = StrokeCap.Round
      drawLine(
        color.value,
        start = Offset(0f, size.height / 2),
        end = Offset(size.width, size.height / 2),
        strokeWidth = strokeWidth,
        cap = strokeCap
      )
      drawLine(
        color.value,
        start = Offset(size.width / 2, 0f),
        end = Offset(size.width / 2, size.height),
        strokeWidth = strokeWidth,
        cap = strokeCap
      )
    }
  }
}

val slowLoadingImage = InlineContent {
  var loaded by rememberSaveable { mutableStateOf(false) }
  LaunchedEffect(loaded) {
    if (!loaded) {
      delay(3000)
      loaded = true
    }
  }

  if (!loaded) {
    LoadingSpinner()
  } else {
    Box(Modifier.clickable(onClick = { loaded = false })) {
      val size = remember { Animatable(16f) }
      LaunchedEffect(Unit) { size.animateTo(100f) }
      Picture(Modifier.size(size.value.sp.toDp()))
      Text(
        "click to refresh",
        modifier = Modifier
          .padding(3.dp)
          .align(Alignment.Center),
        fontSize = 8.sp,
        style = TextStyle(background = Color.LightGray)
      )
    }
  }
}

@Composable private fun LoadingSpinner() {
  val alpha = remember { Animatable(1f) }
  LaunchedEffect(Unit) {
    val anim = infiniteRepeatable<Float>(
      animation = keyframes {
        durationMillis = 500
        0f at 250
        1f at 500
      })
    alpha.animateTo(0f, anim)
  }
  Text(
    "⏳",
    fontSize = 3.em,
    modifier = Modifier
      .wrapContentSize(Alignment.Center)
      .graphicsLayer(alpha = alpha.value)
  )
}

@Composable private fun Picture(modifier: Modifier) {
  Canvas(modifier) {
    drawRect(Color.LightGray)
    drawLine(Color.Red, Offset(0f, 0f), Offset(size.width, size.height))
    drawLine(Color.Red, Offset(0f, size.height), Offset(size.width, 0f))
  }
}

@OptIn(ExperimentalStdlibApi::class)
private fun Builder.appendPreviewSentence(
  format: Format,
  text: String = format.javaClass.simpleName.replaceFirstChar { it.lowercase() }
) {
  append("Here is some ")
  withFormat(format) {
    append(text)
  }
  append(" text. ")
}



================================================
FILE: docs/index.md
================================================
# Overview

[![Maven Central](https://img.shields.io/maven-central/v/com.halilibo.compose-richtext/richtext-ui.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.halilibo.compose-richtext%22)
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)

Compose Richtext is a collection of Compose libraries for working with rich text formatting and
Markdown rendering.  

`richtext-ui`, `richtext-markdown`, `richtext-commonmark`, and `richtext-ui-material`|`richtext-ui-material3` are Kotlin Multiplatform(KMP) Compose Libraries with the exception of iOS.
All these modules can be used in Android and Desktop Compose apps. 

Each library is documented separately, see the navigation menu for the list. This site also includes
an API reference.

!!! warning
    This project is currently on its way to reach `1.0.0` release. The timeline is not clear and the release date will remain TBD for a while.
    There are no tests and some things might be broken or very non-performant.

    The API may also change between releases without deprecation cycles.

## Getting started

These libraries are published to Maven Central, so just add a Gradle dependency:

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:<LIBRARY-ARTIFACT>:${richtext_version}")
}
```

There is no difference for KMP artifacts. For instance, if you are adding `richtext-ui` to a Kotlin Multiplatform module

```kotlin
val commonMain by getting {
  dependencies {
    implementation("com.halilibo.compose-richtext:richtext-ui:${richtext_version}")
  }
}
```

### Library Artifacts

The `LIBRARY_ARTIFACT`s for each individual library can be found on their respective pages.

## Samples

Please check out [Android](https://github.com/halilozercan/compose-richtext/tree/main/android-sample) and [Desktop](https://github.com/halilozercan/compose-richtext/tree/main/desktop-sample)
projects to see various use cases of RichText in both platforms.

================================================
FILE: docs/richtext-commonmark.md
================================================
# Commonmark Markdown

[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)
[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)

Library for parsing and rendering Markdown in Compose using [CommonMark](https://github.com/commonmark/commonmark-java)
library/spec to parse, and [richtext-markdown](../richtext-markdown/) to render.

## Gradle

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:richtext-commonmark:${richtext_version}")
}
```

## Parsing

`richtext-markdown` module renders a given Markdown Abstract Syntax Tree. It accepts a root 
`AstNode`. This library gives you a parser called `CommonmarkAstNodeParser` to easily convert any 
String to an `AstNode` that represents the Markdown tree.

```kotlin
    val parser = CommonmarkAstNodeParser()
    val astNode = parser.parse(
        """
        # Demo
        
        Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it.
        
        1. First ordered list item
        2. Another item
            * Unordered sub-list.
        3. And another item.
            You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
        
        * Unordered list can use asterisks
        - Or minuses
        + Or pluses
        """.trimIndent()
    )
    // ...
  
    RichTextScope.BasicMarkdown(astNode)
```

## Rendering

The simplest way to render markdown is just pass a string to the [`Markdown`](../api/richtext-commonmark/com.halilibo.richtext.markdown/-markdown.html)
composable under RichText scope:

~~~kotlin
RichText(
  modifier = Modifier.padding(16.dp)
) {
  Markdown(
    """
    # Demo

    Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it.

    1. First ordered list item
    2. Another item
        * Unordered sub-list.
    3. And another item.
        You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).

    * Unordered list can use asterisks
    - Or minuses
    + Or pluses
    ---

    ```javascript
    var s = "code blocks use monospace font";
    alert(s);
    ```

    Markdown | Table | Extension
    --- | --- | ---
    *renders* | `beautiful images` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1")
    1 | 2 | 3

    > Blockquotes are very handy in email to emulate reply text.
    > This line is part of the same quote.
    """.trimIndent()
  )
}
~~~

Which produces something like this:

![markdown demo](img/markdown-demo.png)

## [`MarkdownParseOptions`](../api/richtext-commonmark/com.halilibo.richtext.commonmark/-markdown-parse-options.html)

Passing `MarkdownParseOptions` into either `Markdown` composable or `CommonmarkAstNodeParser.parse` method provides the ability to control some aspects of the markdown parser:

```kotlin
val markdownParseOptions = MarkdownParseOptions(
  autolink = false
)

Markdown(
  markdownParseOptions = markdownParseOptions
)
```


================================================
FILE: docs/richtext-markdown.md
================================================
# Markdown

[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)
[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)

Library for rendering Markdown tree that is defined as an `AstNode`. This module would be useless
for someone who is looking to just render a Markdown string. Please check out 
`richtext-commonmark` for such features. `richtext-markdown` behaves as sort of a building block.
You can create your own parser or use 3rd party ones that converts any Markdown string to an 
`AstNode` tree.

## Gradle

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:richtext-markdown:${richtext_version}")
}
```

## Rendering

The simplest way to render markdown is just pass an `AstNode` to the [`BasicMarkdown`](../api/richtext-markdown/com.halilibo.richtext.markdown/-basic-markdown.html)
composable under RichText scope:

~~~kotlin
RichText(
  modifier = Modifier.padding(16.dp)
) {
  // requires richtext-commonmark module.
  val parser = remember(options) { CommonmarkAstNodeParser(options) }
  val astNode = remember(parser) {
    parser.parse(
      """
        # Demo
        
        Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it.
        
        1. First ordered list item
        2. Another item
            * Unordered sub-list.
        3. And another item.
            You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
        
        * Unordered list can use asterisks
        - Or minuses
        + Or pluses
        """.trimIndent()
    )
  }
  BasicMarkdown(astNode)
}
~~~


================================================
FILE: docs/richtext-ui-material.md
================================================
# Richtext UI Material

[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)
[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)

Library that makes RichText compatible with Material design in Compose.

## Gradle

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:richtext-ui-material:${richtext_version}")
}
```

## Usage

Material RichText library provides a single composable called `RichText` which automatically passes
down Material theming attributes to `BasicRichText`. 

### [`RichText`](../api/richtext-ui-material/com.halilibo.richtext.ui.material/-rich-text.html)

`RichText` composable wraps around regular `BasicRichText` while introducing the necessary integration
dependencies. `RichText` shares the exact arguments with regular `BasicRichText`.

```kotlin
RichText(modifier = Modifier.background(color = Color.White)) {
  Heading(0, "Paragraphs")
  Text("Simple paragraph.")
  ...
}
```


================================================
FILE: docs/richtext-ui-material3.md
================================================
# Richtext UI Material 3

[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)
[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)

Library that makes RichText compatible with Material design in Compose.

## Gradle

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:richtext-ui-material3:${richtext_version}")
}
```

## Usage

Material3 RichText library provides a single composable called `RichText` which automatically passes
down Material3 theming attributes to `BasicRichText`.

### [`RichText`](../api/richtext-ui-material/com.halilibo.richtext.ui.material3/-rich-text.html)

`RichText` composable wraps around regular `BasicRichText` while introducing the necessary integration
dependencies. `RichText` shares the exact arguments with regular `BasicRichText`.

```kotlin
RichText(modifier = Modifier.background(color = Color.White)) {
  Heading(0, "Paragraphs")
  Text("Simple paragraph.")
  ...
}
```


================================================
FILE: docs/richtext-ui.md
================================================
# Richtext UI

[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)
[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)

A library of Composables for formatting text using higher-level concepts than are not supported by
compose foundation, such as "ordered lists" and "headings".

Richtext UI is a base library that is non-opinionated about higher level design requirements.
If you are already using `MaterialTheme` in your compose app, you can jump to [RichText UI Material](../richtext-ui-material/index.html)
for a quick start. There is also Material3 flavor at [RichText UI Material3](../richtext-ui-material3/index.html)

## Gradle

```kotlin
dependencies {
  implementation("com.halilibo.compose-richtext:richtext-ui:${richtext_version}")
}
```

## [`BasicRichText`](../api/richtext-ui/com.halilibo.richtext.ui/-basic-rich-text.html)

Richtext UI does not depend on Material artifact of Compose. Design agnostic API allows anyone
to adopt Richtext UI and its extensions like Markdown to their own design and typography systems.
Hence, just like `foundation` and `material` modules of Compose, this library also names the 
building block with `Basic` prefix.

If you are planning to adopt RichText within your design system, please go ahead and check out [`RichText Material`](../richtext-ui-material/index.html)
for inspiration.

## [`RichTextScope`](../api/richtext-ui/com.halilibo.richtext.ui/-rich-text-scope/index.html)

`RichTextScope` is a context wrapper around Composables that integrate and play well within Richtext
content. 

## [`RichTextThemeProvider`](../api/richtext-ui/com.halilibo.richtext.ui/-rich-text-theme-provider.html)

Entry point for integrating app's own typography and theme system with BasicRichText.

API for this integration is highly influenced by how compose-material theming
is designed. RichText library assumes that almost all Theme/Design systems would
have composition locals that provide a TextStyle downstream.

Moreover, text style should not include text color by best practice. Content color
exists to figure out text color in the current context. Light/Dark theming leverages content
color to influence not just text but other parts of theming as well.

## Example

Open the `Demo.kt` file in the `sample` module to play with this. Although the mentioned demo
uses Material integrated version of `RichText`, they share exactly the same API.

```kotlin
BasicRichText(
  modifier = Modifier.background(color = Color.White)
) {
  Heading(0, "Paragraphs")
  Text("Simple paragraph.")
  Text("Paragraph with\nmultiple lines.")
  Text("Paragraph with really long line that should be getting wrapped.")

  Heading(0, "Lists")
  Heading(1, "Unordered")
  ListDemo(listType = Unordered)
  Heading(1, "Ordered")
  ListDemo(listType = Ordered)

  Heading(0, "Horizontal Line")
  Text("Above line")
  HorizontalRule()
  Text("Below line")

  Heading(0, "Code Block")
  CodeBlock(
    """
      {
        "Hello": "world!"
      }
    """.trimIndent()
  )

  Heading(0, "Block Quote")
  BlockQuote {
    Text("These paragraphs are quoted.")
    Text("More text.")
    BlockQuote {
      Text("Nested block quote.")
    }
  }

  Heading(0, "Info Panel")
  InfoPanel(InfoPanelType.Primary, "Only text primary info panel")
  InfoPanel(InfoPanelType.Success) {
    Column {
      Text("Successfully sent some data")
      HorizontalRule()
      BlockQuote {
        Text("This is a quote")
      }
    }
  }

  Heading(0, "Table")
  Table(headerRow = {
    cell { Text("Column 1") }
    cell { Text("Column 2") }
  }) {
    row {
      cell { Text("Hello") }
      cell {
        CodeBlock("Foo bar")
      }
    }
    row {
      cell {
        BlockQuote {
          Text("Stuff")
        }
      }
      cell { Text("Hello world this is a really long line that is going to wrap hopefully") }
    }
  }
}
```

Looks like this:

![demo rendering](img/richtext-demo.png)


================================================
FILE: gen_dokka_docs.sh
================================================
#!/bin/bash

# Fail on any error
set -ex

DOCS_ROOT=docs-gen

[ -d $DOCS_ROOT ] && rm -r $DOCS_ROOT
mkdir $DOCS_ROOT

# Clear out the old API docs
[ -d docs/api ] && rm -r docs/api
# Build the docs with dokka
./gradlew dokkaGenerate --stacktrace

# Create a copy of our docs at our $DOCS_ROOT
cp -a docs/* $DOCS_ROOT

# Convert docs/xxx.md links to just xxx/
sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md

# Finally delete all of the backup files
find . -name '*.bak' -delete

================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

# Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308)
systemProp.org.gradle.internal.publish.checksums.insecure=true

GROUP=com.halilibo.compose-richtext
VERSION_NAME=1.0.0-alpha04

POM_DESCRIPTION=A collection of Compose libraries for advanced text formatting and alternative display types.

POM_INCEPTION_YEAR=2020
POM_URL=https://github.com/halilozercan/compose-richtext
POM_SCM_URL=https://github.com/halilozercan/compose-richtext/
POM_SCM_CONNECTION=scm:git:git://github.com/halilozercan/compose-richtext.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/halilozercan/compose-richtext.git
POM_LICENSE_NAME=The Apache Software License, Version 2.0
POM_LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENSE_DIST=repo
POM_DEVELOPER_ID=halilozercan
POM_DEVELOPER_NAME=Halil Ozercan

org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers

kotlin.mpp.stability.nowarn=true
kotlin.mpp.androidSourceSetLayoutVersion=2
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: mkdocs.yml
================================================
site_name: Compose Richtext
repo_name: compose-richtext
repo_url: https://github.com/halilozercan/compose-richtext
site_description: "A collection of Compose libraries for advanced text formatting and alternative display types."
site_author: Halil Ozercan
site_url: https://halilibo.com/compose-richtext
remote_branch: gh-pages
edit_uri: edit/main/docs/

docs_dir: docs-gen

theme:
  name: material
  icon:
    repo: fontawesome/brands/github
  features:
    - navigation.instant
    - toc.autohide

markdown_extensions:
  - toc:
      permalink: true
  - pymdownx.superfences
  - pymdownx.tabbed
  - admonition

nav:
  - index.md
  - richtext-ui-material.md
  - richtext-ui-material3.md
  - richtext-ui.md
  - richtext-markdown.md
  - richtext-commonmark.md
  - 'API Reference': api/
  - Changelog: https://github.com/halilozercan/compose-richtext/releases


================================================
FILE: richtext-commonmark/build.gradle.kts
================================================
plugins {
  id("richtext-kmp-library")
  id("org.jetbrains.dokka")
}

kotlin {

  android {
    namespace = "com.halilibo.richtext.commonmark"
  }
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation(compose.runtime)
        api(project(":richtext-ui"))
        api(project(":richtext-markdown"))
      }
    }
    val commonTest by getting

    val jvmAndroidMain by creating {
      dependsOn(commonMain)
      dependencies {
        implementation(Commonmark.core)
        implementation(Commonmark.tables)
        implementation(Commonmark.strikethrough)
        implementation(Commonmark.autolink)
      }
    }

    val jvmAndroidTest by creating {
      dependsOn(commonTest)
      dependencies {
        implementation(Kotlin.Test.jdk)
      }
    }

    val androidMain by getting {
      dependsOn(jvmAndroidMain)
    }

    val jvmMain by getting {
      dependsOn(jvmAndroidMain)
    }

    val jvmTest by getting {
      dependsOn(jvmAndroidTest)
    }
  }
}


================================================
FILE: richtext-commonmark/gradle.properties
================================================
POM_NAME=Compose Richtext Commonmark
POM_DESCRIPTION=A library for rendering markdown in Compose using the Commonmark library.

================================================
FILE: richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/CommonMarkdownParseOptions.kt
================================================
package com.halilibo.richtext.commonmark

/**
 * Allows configuration of the Markdown parser
 *
 * @param autolink Detect plain text links and turn them into Markdown links.
 */
public class CommonMarkdownParseOptions(
  public val autolink: Boolean
) {

  override fun toString(): String {
    return "CommonMarkdownParseOptions(autolink=$autolink)"
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is CommonMarkdownParseOptions) return false

    return autolink == other.autolink
  }

  override fun hashCode(): Int {
    return autolink.hashCode()
  }

  public fun copy(
    autolink: Boolean = this.autolink
  ): CommonMarkdownParseOptions = CommonMarkdownParseOptions(
    autolink = autolink
  )

  public companion object {
    public val Default: CommonMarkdownParseOptions = CommonMarkdownParseOptions(
      autolink = true
    )
  }
}


================================================
FILE: richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt
================================================
package com.halilibo.richtext.commonmark

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import com.halilibo.richtext.markdown.AstBlockNodeComposer
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.ui.RichTextScope

/**
 * A composable that renders Markdown content according to Commonmark specification using RichText.
 *
 * @param content Markdown text. No restriction on length.
 * @param markdownParseOptions Options for the Markdown parser.
 * @param astBlockNodeComposer An interceptor to take control of composing any block type node's
 * rendering. Use it to render images, html text, tables with your own components.
 */
@Composable
public fun RichTextScope.Markdown(
  content: String,
  markdownParseOptions: CommonMarkdownParseOptions = CommonMarkdownParseOptions.Default,
  astBlockNodeComposer: AstBlockNodeComposer? = null
) {
  val commonmarkAstNodeParser = remember(markdownParseOptions) {
    CommonmarkAstNodeParser(markdownParseOptions)
  }

  val astRootNode by produceState<AstNode?>(
    initialValue = null,
    key1 = commonmarkAstNodeParser,
    key2 = content
  ) {
    value = commonmarkAstNodeParser.parse(content)
  }

  astRootNode?.let {
    BasicMarkdown(astNode = it, astBlockNodeComposer = astBlockNodeComposer)
  }
}

/**
 * A helper class that can convert any text content into an ASTNode tree and return its root.
 */
public expect class CommonmarkAstNodeParser(
  options: CommonMarkdownParseOptions = CommonMarkdownParseOptions.Default
) {

  /**
   * Parse markdown content and return Abstract Syntax Tree(AST).
   *
   * @param text Markdown text to be parsed.
   * @param options Options for the Commonmark Markdown parser.
   */
  public fun parse(text: String): AstNode
}

================================================
FILE: richtext-commonmark/src/jvmAndroidMain/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt
================================================
package com.halilibo.richtext.commonmark

import com.halilibo.richtext.markdown.node.AstBlockQuote
import com.halilibo.richtext.markdown.node.AstCode
import com.halilibo.richtext.markdown.node.AstDocument
import com.halilibo.richtext.markdown.node.AstEmphasis
import com.halilibo.richtext.markdown.node.AstFencedCodeBlock
import com.halilibo.richtext.markdown.node.AstHardLineBreak
import com.halilibo.richtext.markdown.node.AstHeading
import com.halilibo.richtext.markdown.node.AstHtmlBlock
import com.halilibo.richtext.markdown.node.AstHtmlInline
import com.halilibo.richtext.markdown.node.AstImage
import com.halilibo.richtext.markdown.node.AstIndentedCodeBlock
import com.halilibo.richtext.markdown.node.AstLink
import com.halilibo.richtext.markdown.node.AstLinkReferenceDefinition
import com.halilibo.richtext.markdown.node.AstListItem
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.markdown.node.AstNodeLinks
import com.halilibo.richtext.markdown.node.AstNodeType
import com.halilibo.richtext.markdown.node.AstOrderedList
import com.halilibo.richtext.markdown.node.AstParagraph
import com.halilibo.richtext.markdown.node.AstSoftLineBreak
import com.halilibo.richtext.markdown.node.AstStrikethrough
import com.halilibo.richtext.markdown.node.AstStrongEmphasis
import com.halilibo.richtext.markdown.node.AstTableBody
import com.halilibo.richtext.markdown.node.AstTableCell
import com.halilibo.richtext.markdown.node.AstTableCellAlignment
import com.halilibo.richtext.markdown.node.AstTableHeader
import com.halilibo.richtext.markdown.node.AstTableRoot
import com.halilibo.richtext.markdown.node.AstTableRow
import com.halilibo.richtext.markdown.node.AstText
import com.halilibo.richtext.markdown.node.AstThematicBreak
import com.halilibo.richtext.markdown.node.AstUnorderedList
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.ext.gfm.strikethrough.Strikethrough
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TableBlock
import org.commonmark.ext.gfm.tables.TableBody
import org.commonmark.ext.gfm.tables.TableCell
import org.commonmark.ext.gfm.tables.TableCell.Alignment.CENTER
import org.commonmark.ext.gfm.tables.TableCell.Alignment.LEFT
import org.commonmark.ext.gfm.tables.TableCell.Alignment.RIGHT
import org.commonmark.ext.gfm.tables.TableHead
import org.commonmark.ext.gfm.tables.TableRow
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.BlockQuote
import org.commonmark.node.BulletList
import org.commonmark.node.Code
import org.commonmark.node.CustomBlock
import org.commonmark.node.CustomNode
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import org.commonmark.node.Image
import org.commonmark.node.IndentedCodeBlock
import org.commonmark.node.Link
import org.commonmark.node.LinkReferenceDefinition
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser

/**
 * Holds the data for a pending conversion task in the iterative tree traversal.
 */
private class ConvertWorkItem(
  val startNode: Node,
  val parentAstNode: AstNode?,
  val initialPrev: AstNode?,
  val onFirstCreated: (AstNode?) -> Unit
)

/**
 * Maps a CommonMark [Node] to its corresponding [AstNodeType].
 * Returns null for unrecognized node types (CustomNode, CustomBlock, etc.).
 */
private fun convertNodeType(node: Node): AstNodeType? = when (node) {
  is BlockQuote -> AstBlockQuote
  is BulletList -> AstUnorderedList(bulletMarker = node.bulletMarker)
  is Code -> AstCode(literal = node.literal)
  is Document -> AstDocument
  is Emphasis -> AstEmphasis(delimiter = node.openingDelimiter)
  is FencedCodeBlock -> AstFencedCodeBlock(
    literal = node.literal,
    fenceChar = node.fenceChar,
    fenceIndent = node.fenceIndent,
    fenceLength = node.fenceLength,
    info = node.info
  )
  is HardLineBreak -> AstHardLineBreak
  is Heading -> AstHeading(
    level = node.level
  )
  is ThematicBreak -> AstThematicBreak
  is HtmlInline -> AstHtmlInline(
    literal = node.literal
  )
  is HtmlBlock -> AstHtmlBlock(
    literal = node.literal
  )
  is Image -> {
    if (node.destination == null) {
      null
    }
    else {
      AstImage(
        title = node.title ?: "",
        destination = node.destination
      )
    }
  }
  is IndentedCodeBlock -> AstIndentedCodeBlock(
    literal = node.literal
  )
  is Link -> AstLink(
    title = node.title ?: "",
    destination = node.destination
  )
  is ListItem -> AstListItem
  is OrderedList -> AstOrderedList(
    startNumber = node.startNumber,
    delimiter = node.delimiter
  )
  is Paragraph -> AstParagraph
  is SoftLineBreak -> AstSoftLineBreak
  is StrongEmphasis -> AstStrongEmphasis(
    delimiter = node.openingDelimiter
  )
  is Text -> AstText(
    literal = node.literal
  )
  is LinkReferenceDefinition -> AstLinkReferenceDefinition(
    title = node.title ?: "",
    destination = node.destination,
    label = node.label
  )
  is TableBlock -> AstTableRoot
  is TableHead -> AstTableHeader
  is TableBody -> AstTableBody
  is TableRow -> AstTableRow
  is TableCell -> AstTableCell(
    header = node.isHeader,
    alignment = when (node.alignment) {
      LEFT -> AstTableCellAlignment.LEFT
      CENTER -> AstTableCellAlignment.CENTER
      RIGHT -> AstTableCellAlignment.RIGHT
      null -> AstTableCellAlignment.LEFT
      else -> AstTableCellAlignment.LEFT
    }
  )
  is Strikethrough -> AstStrikethrough(
    node.openingDelimiter
  )
  is CustomNode -> null
  is CustomBlock -> null
  else -> null
}

/**
 * Converts common-markdown tree to AstNode tree iteratively using an explicit stack,
 * avoiding StackOverflowError on deeply nested or long markdown documents.
 */
internal fun convert(
  node: Node?,
  parentNode: AstNode? = null,
  previousNode: AstNode? = null,
): AstNode? {
  node ?: return null

  var result: AstNode? = null
  val stack = ArrayDeque<ConvertWorkItem>()
  stack.addLast(ConvertWorkItem(node, parentNode, previousNode) { result = it })

  while (stack.isNotEmpty()) {
    val item = stack.removeLast()

    var prev: AstNode? = null
    var firstCreated: AstNode? = null
    var cmNode: Node? = item.startNode
    var nullTypeNode: Node? = null

    // Iterate through siblings instead of recursing
    while (cmNode != null) {
      val nodeType = convertNodeType(cmNode)
      val newNode = nodeType?.let {
        AstNode(it, AstNodeLinks(
          parent = item.parentAstNode,
          previous = prev ?: item.initialPrev
        ))
      }

      if (newNode != null) {
        if (firstCreated == null) firstCreated = newNode
        prev?.links?.next = newNode

        // Push child processing onto the explicit stack instead of recursing
        val child = cmNode.firstChild
        if (child != null) {
          stack.addLast(ConvertWorkItem(child, newNode, null) { newNode.links.firstChild = it })
        }

        prev = newNode
        cmNode = cmNode.next
      } else {
        // Unrecognized node type — stop sibling chain (preserves original behavior)
        nullTypeNode = cmNode
        cmNode = null
      }
    }

    // Set lastChild on the parent, matching the original recursive behavior
    if (nullTypeNode != null) {
      if (nullTypeNode.next == null) {
        item.parentAstNode?.links?.lastChild = null
      }
    } else {
      item.parentAstNode?.links?.lastChild = prev
    }

    item.onFirstCreated(firstCreated)
  }

  return result
}

public actual class CommonmarkAstNodeParser actual constructor(
  options: CommonMarkdownParseOptions
) {

  private val parser = Parser.builder()
    .extensions(
      listOfNotNull(
        TablesExtension.create(),
        StrikethroughExtension.create(),
        if (options.autolink) AutolinkExtension.create() else null
      )
    )
    .build()

  public actual fun parse(text: String): AstNode {
    val commonmarkNode = parser.parse(text)
      ?: throw IllegalArgumentException(
        "Could not parse the given text content into a meaningful Markdown representation!"
      )

    return convert(commonmarkNode)
      ?: throw IllegalArgumentException(
        "Could not convert the generated Commonmark Node into an ASTNode!"
      )
  }
}



================================================
FILE: richtext-commonmark/src/jvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt
================================================
package com.halilibo.richtext.markdown

import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.convert
import com.halilibo.richtext.markdown.node.AstBlockQuote
import com.halilibo.richtext.markdown.node.AstDocument
import com.halilibo.richtext.markdown.node.AstHeading
import com.halilibo.richtext.markdown.node.AstImage
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.markdown.node.AstNodeLinks
import com.halilibo.richtext.markdown.node.AstParagraph
import com.halilibo.richtext.markdown.node.AstText
import org.commonmark.node.Document
import org.commonmark.node.Image
import org.commonmark.node.Paragraph
import org.commonmark.node.Text
import org.junit.Test
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
import kotlin.test.fail

internal class AstNodeConvertKtTest {

  private val parser = CommonmarkAstNodeParser(CommonMarkdownParseOptions.Default)

  @Test
  fun `when image without title is converted, then the content description is empty`() {
    val destination = "/url"
    val image = Image(destination, null)

    val result = convert(image)

    assertEquals(
      expected = AstNode(
        type = AstImage(title = "", destination = destination),
        links = AstNodeLinks()
      ),
      actual = result
    )
  }

  @Test
  fun `tree links are correctly wired for a document with siblings`() {
    val root = parser.parse("# Heading\n\nParagraph text")

    // Root should be a Document
    assertEquals(AstDocument, root.type)
    assertNull(root.links.parent)

    // First child should be the heading
    val heading = root.links.firstChild
    assertNotNull(heading)
    assertEquals(AstHeading(level = 1), heading.type)
    assertSame(root, heading.links.parent)
    assertNull(heading.links.previous)

    // Second child should be the paragraph
    val paragraph = heading.links.next
    assertNotNull(paragraph)
    assertEquals(AstParagraph, paragraph.type)
    assertSame(root, paragraph.links.parent)
    assertSame(heading, paragraph.links.previous)
    assertNull(paragraph.links.next)

    // lastChild should point to the paragraph
    assertSame(paragraph, root.links.lastChild)
  }

  @Test
  fun `tree links are correctly wired for nested structures`() {
    val root = parser.parse("> quoted text")

    val blockquote = root.links.firstChild
    assertNotNull(blockquote)
    assertEquals(AstBlockQuote, blockquote.type)
    assertSame(root, blockquote.links.parent)

    val paragraph = blockquote.links.firstChild
    assertNotNull(paragraph)
    assertEquals(AstParagraph, paragraph.type)
    assertSame(blockquote, paragraph.links.parent)

    val text = paragraph.links.firstChild
    assertNotNull(text)
    assertEquals(AstText(literal = "quoted text"), text.type)
    assertSame(paragraph, text.links.parent)
  }

  @Test
  fun `document with many sibling paragraphs does not overflow`() {
    // 2000 paragraphs would cause ~2000 frames of sibling recursion
    val markdown = (1..2000).joinToString("\n\n") { "Paragraph $it" }
    val root = parser.parse(markdown)

    assertEquals(AstDocument, root.type)

    // Walk the sibling chain and count paragraphs
    var count = 0
    var node = root.links.firstChild
    while (node != null) {
      assertEquals(AstParagraph, node.type)
      count++
      node = node.links.next
    }
    assertEquals(2000, count)

    // lastChild should be the final paragraph
    assertNotNull(root.links.lastChild)
    assertNull(root.links.lastChild!!.links.next)
  }

  @Test
  fun `deeply nested blockquotes do not overflow`() {
    // 500 levels of nested blockquotes would cause ~500 frames of child recursion
    val markdown = ">".repeat(500) + " deep text"
    val root = parser.parse(markdown)

    assertEquals(AstDocument, root.type)

    // Walk down the child chain counting blockquotes
    var depth = 0
    var node = root.links.firstChild
    while (node != null && node.type is AstBlockQuote) {
      depth++
      node = node.links.firstChild
    }
    assertEquals(500, depth)

    // The innermost blockquote should contain a paragraph with text
    assertNotNull(node)
    assertEquals(AstParagraph, node.type)
  }

  /**
   * Proves that the sibling chain depth we test would overflow a recursive implementation
   * on a constrained thread stack (similar to Android's ~1MB default), while our iterative
   * convert() handles it without issue.
   */
  @Test
  fun `convert handles long sibling chains that would overflow a recursive implementation`() {
    val siblingCount = 5000
    // Build a CommonMark tree directly: Document -> Paragraph("1") -> Paragraph("2") -> ...
    val doc = Document()
    for (i in 1..siblingCount) {
      val para = Paragraph()
      para.appendChild(Text("$i"))
      doc.appendChild(para)
    }

    val stackSize = 256L * 1024 // 256KB — smaller than Android's default ~1MB

    // First, prove this stack size is too small for equivalent-depth recursion.
    // A simple recursive chain of siblingCount depth will overflow.
    val recursionOverflowed = AtomicReference<Boolean>(false)
    val recursionThread = Thread(null, {
      try {
        countRecursively(siblingCount)
      } catch (_: StackOverflowError) {
        recursionOverflowed.set(true)
      }
    }, "recursion-test", stackSize)
    recursionThread.start()
    recursionThread.join()
    assertTrue(
      recursionOverflowed.get(),
      "Expected StackOverflowError for $siblingCount recursive calls on ${stackSize / 1024}KB stack"
    )

    // Now prove our iterative convert() handles the same depth on the same stack size.
    val convertError = AtomicReference<Throwable?>(null)
    val convertResult = AtomicReference<AstNode?>(null)
    val convertThread = Thread(null, {
      try {
        convertResult.set(convert(doc))
      } catch (e: Throwable) {
        convertError.set(e)
      }
    }, "convert-test", stackSize)
    convertThread.start()
    convertThread.join()

    val error = convertError.get()
    if (error != null) {
      fail("convert() should not throw on a long sibling chain, but threw: $error")
    }

    val root = convertResult.get()
    assertNotNull(root, "convert() should return a non-null root")
    assertEquals(AstDocument, root.type)

    // Verify the full sibling chain was converted
    var count = 0
    var node = root.links.firstChild
    while (node != null) {
      count++
      node = node.links.next
    }
    assertEquals(siblingCount, count)
  }
}

/** Simple recursive function that recurses [n] times to demonstrate stack overflow. */
private fun countRecursively(n: Int): Int {
  if (n <= 0) return 0
  return 1 + countRecursively(n - 1)
}


================================================
FILE: richtext-markdown/build.gradle.kts
================================================
plugins {
  id("richtext-kmp-library")
}

kotlin {
  android {
    namespace = "com.halilibo.richtext.markdown"
  }
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation(compose.runtime)
        implementation(compose.foundation)
        api(project(":richtext-ui"))
      }
    }
    val commonTest by getting

    val androidMain by getting {
      dependencies {
        implementation(Compose.coil)
        implementation(Compose.coilHttp)
      }
    }

    val jvmMain by getting {
      dependencies {
        implementation(compose.desktop.currentOs)
        implementation(Network.okHttp)
      }
    }

    val jvmTest by getting {
      dependencies {
        implementation(Kotlin.Test.jdk)
      }
    }
  }
}


================================================
FILE: richtext-markdown/gradle.properties
================================================
POM_NAME=Compose Richtext Markdown
POM_DESCRIPTION=A library for rendering markdown represented as an AST in Compose.

================================================
FILE: richtext-markdown/src/androidMain/AndroidManifest.xml
================================================
<manifest />

================================================
FILE: richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt
================================================
package com.halilibo.richtext.markdown

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.string.Text
import com.halilibo.richtext.ui.string.richTextString

@Composable
internal actual fun RichTextScope.HtmlBlock(content: String) {
  val richTextString = remember(content) {
    richTextString {
      withAnnotatedString {
        append(AnnotatedString.Companion.fromHtml(content))
      }
    }
  }
  Text(richTextString)
}


================================================
FILE: richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/MarkdownImage.kt
================================================
package com.halilibo.richtext.markdown

import android.annotation.SuppressLint
import android.util.Base64
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.size.Size

private val DEFAULT_IMAGE_SIZE = 64.dp

/**
 * Implementation of MarkdownImage by using Coil library for Android.
 */
@Composable
internal actual fun MarkdownImage(
  url: String,
  contentDescription: String?,
  modifier: Modifier,
  contentScale: ContentScale
) {
  val data = if (url.startsWith("data:image") && url.contains("base64")) {
    val base64ImageString = url.substringAfter("base64,")
    Base64.decode(base64ImageString, Base64.DEFAULT)
  } else {
    url
  }
  val painter = rememberAsyncImagePainter(
    ImageRequest.Builder(LocalContext.current)
      .data(data = data)
      .size(Size.ORIGINAL)
      .crossfade(true)
      .build()
  )

  @SuppressLint("UnusedBoxWithConstraintsScope")
  BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {
    val painterState by painter.state.collectAsState()
    val sizeModifier = renderInSize(painterState.painter?.intrinsicSize)

    Image(
      painter = painter,
      contentDescription = contentDescription,
      modifier = sizeModifier,
      contentScale = contentScale
    )
  }
}

@Composable
public fun BoxWithConstraintsScope.renderInSize(
  painterIntrinsicSize: androidx.compose.ui.geometry.Size?,
): Modifier {
  val density = LocalDensity.current

  val sizeModifier = if (painterIntrinsicSize != null &&
    painterIntrinsicSize.isSpecified &&
    painterIntrinsicSize.width != Float.POSITIVE_INFINITY &&
    painterIntrinsicSize.height != Float.POSITIVE_INFINITY
  ) {
    val width = painterIntrinsicSize.width
    val height = painterIntrinsicSize.height
    val scale = if (width > constraints.maxWidth) {
      constraints.maxWidt
Download .txt
gitextract_y6p53me7/

├── .github/
│   └── workflows/
│       ├── android.yml
│       ├── docs.yml
│       └── publish.yml
├── .gitignore
├── .idea/
│   └── codeStyles/
│       ├── Project.xml
│       └── codeStyleConfig.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android-sample/
│   ├── build.gradle.kts
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── com/
│           │       └── zachklipp/
│           │           └── richtext/
│           │               └── sample/
│           │                   ├── Demo.kt
│           │                   ├── LazyMarkdownSample.kt
│           │                   ├── MarkdownSample.kt
│           │                   ├── RichTextSample.kt
│           │                   ├── SampleActivity.kt
│           │                   ├── SampleLauncher.kt
│           │                   ├── SampleTheme.kt
│           │                   ├── ScreenPreview.kt
│           │                   └── TextDemo.kt
│           └── res/
│               ├── drawable/
│               │   └── ic_launcher_background.xml
│               ├── drawable-v24/
│               │   └── ic_launcher_foreground.xml
│               ├── mipmap-anydpi-v26/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               └── values/
│                   ├── colors.xml
│                   ├── strings.xml
│                   └── styles.xml
├── build.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               ├── Dependencies.kt
│               └── richtext-kmp-library.gradle.kts
├── desktop-sample/
│   ├── build.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── desktop/
│                               ├── MarkdownSampleApp.kt
│                               └── RichTextSampleApp.kt
├── docs/
│   ├── index.md
│   ├── richtext-commonmark.md
│   ├── richtext-markdown.md
│   ├── richtext-ui-material.md
│   ├── richtext-ui-material3.md
│   └── richtext-ui.md
├── gen_dokka_docs.sh
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── mkdocs.yml
├── richtext-commonmark/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── commonmark/
│       │                       ├── CommonMarkdownParseOptions.kt
│       │                       └── Markdown.kt
│       ├── jvmAndroidMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── commonmark/
│       │                       └── AstNodeConvert.kt
│       └── jvmAndroidTest/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── commonmark/
│                               └── AstNodeConvertKtTest.kt
├── richtext-markdown/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   ├── AndroidManifest.xml
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── markdown/
│       │                       ├── HtmlBlock.kt
│       │                       └── MarkdownImage.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── markdown/
│       │                       ├── BasicMarkdown.kt
│       │                       ├── HtmlBlock.kt
│       │                       ├── MarkdownImage.kt
│       │                       ├── MarkdownRichText.kt
│       │                       ├── RenderTable.kt
│       │                       ├── TraverseUtils.kt
│       │                       └── node/
│       │                           ├── AstNode.kt
│       │                           ├── AstNodeLinks.kt
│       │                           ├── AstNodeType.kt
│       │                           └── AstTable.kt
│       └── jvmMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── markdown/
│                               ├── HtmlBlock.kt
│                               └── RemoteImage.kt
├── richtext-ui/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   ├── AndroidManifest.xml
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       └── CodeBlock.android.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       ├── BasicRichText.kt
│       │                       ├── BlockQuote.kt
│       │                       ├── CodeBlock.kt
│       │                       ├── FormattedList.kt
│       │                       ├── Heading.kt
│       │                       ├── HorizontalRule.kt
│       │                       ├── InfoPanel.kt
│       │                       ├── RichTextLocals.kt
│       │                       ├── RichTextScope.kt
│       │                       ├── RichTextStyle.kt
│       │                       ├── RichTextThemeConfiguration.kt
│       │                       ├── RichTextThemeProvider.kt
│       │                       ├── SimpleTableLayout.kt
│       │                       ├── Table.kt
│       │                       ├── string/
│       │                       │   ├── InlineContent.kt
│       │                       │   ├── RichTextString.kt
│       │                       │   └── Text.kt
│       │                       └── util/
│       │                           ├── ConditionalTapGestureDetector.kt
│       │                           └── UUID.kt
│       ├── jvmAndroidMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── halilibo/
│       │               └── richtext/
│       │                   └── ui/
│       │                       └── util/
│       │                           └── UUID.kt
│       └── jvmMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── CodeBlock.desktop.kt
├── richtext-ui-material/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── AndroidManifest.xml
│       └── commonMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── material/
│                                   └── RichText.kt
├── richtext-ui-material3/
│   ├── build.gradle.kts
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── AndroidManifest.xml
│       └── commonMain/
│           └── kotlin/
│               └── com/
│                   └── halilibo/
│                       └── richtext/
│                           └── ui/
│                               └── material3/
│                                   └── RichText.kt
└── settings.gradle.kts
Condensed preview — 105 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (326K chars).
[
  {
    "path": ".github/workflows/android.yml",
    "chars": 541,
    "preview": "name: Android CI\n\non:\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checko"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 464,
    "preview": "name: Doc Site\n\non:\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/ch"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1069,
    "preview": "name: Publish\n\non:\n  workflow_dispatch:\n\njobs:\n  publish:\n    name: Release build and publish\n    runs-on: ubuntu-latest"
  },
  {
    "path": ".gitignore",
    "chars": 224,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor."
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "chars": 16703,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <option name=\"OTHER_IN"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 143,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3805,
    "preview": "Changelog\n=========\n\n1.0.0-alpha03\n-------\n\nThis release removes the `printing` and `slideshow` modules to focus on the "
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 1710,
    "preview": "# Compose Markdown and Rich Text\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.halilibo.compose-richtext"
  },
  {
    "path": "android-sample/build.gradle.kts",
    "chars": 987,
    "preview": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n  id(\"com.android.application\")\n  kotlin(\"android\")\n  id(\"or"
  },
  {
    "path": "android-sample/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "android-sample/src/main/AndroidManifest.xml",
    "chars": 804,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <uses-permission android:name=\"android.permissi"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt",
    "chars": 3831,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.zachklipp.richtext.sample\n\nimport androidx.comp"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/LazyMarkdownSample.kt",
    "chars": 12372,
    "preview": "package com.zachklipp.richtext.sample\n\nimport android.widget.Toast\nimport androidx.compose.foundation.clickable\nimport a"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt",
    "chars": 14731,
    "preview": "package com.zachklipp.richtext.sample\n\nimport android.widget.Toast\nimport androidx.compose.foundation.clickable\nimport a"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/RichTextSample.kt",
    "chars": 4231,
    "preview": "package com.zachklipp.richtext.sample\n\nimport androidx.annotation.IntRange\nimport androidx.compose.foundation.clickable\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleActivity.kt",
    "chars": 412,
    "preview": "package com.zachklipp.richtext.sample\n\nimport android.os.Bundle\nimport androidx.activity.compose.setContent\nimport andro"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleLauncher.kt",
    "chars": 4607,
    "preview": "package com.zachklipp.richtext.sample\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.Cr"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleTheme.kt",
    "chars": 1774,
    "preview": "package com.zachklipp.richtext.sample\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nim"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/ScreenPreview.kt",
    "chars": 6051,
    "preview": "@file:Suppress(\"DEPRECATION\")\n\npackage com.zachklipp.richtext.sample\n\nimport android.content.Context\nimport android.cont"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt",
    "chars": 7439,
    "preview": "package com.zachklipp.richtext.sample\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.compos"
  },
  {
    "path": "android-sample/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 5280,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "android-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1588,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 268,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <bac"
  },
  {
    "path": "android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 268,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <bac"
  },
  {
    "path": "android-sample/src/main/res/values/colors.xml",
    "chars": 202,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#eeeeee</color>\n  <color name=\"colorPrim"
  },
  {
    "path": "android-sample/src/main/res/values/strings.xml",
    "chars": 76,
    "preview": "<resources>\n  <string name=\"app_name\">Rich Text Sample</string>\n</resources>"
  },
  {
    "path": "android-sample/src/main/res/values/styles.xml",
    "chars": 357,
    "preview": "<resources>\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n    "
  },
  {
    "path": "build.gradle.kts",
    "chars": 2041,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nimport org.jetbrains"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "chars": 872,
    "preview": "repositories {\n  google()\n  mavenCentral()\n}\n\nplugins {\n  `kotlin-dsl`\n  `kotlin-dsl-precompiled-script-plugins`\n}\n\ndepe"
  },
  {
    "path": "buildSrc/src/main/kotlin/Dependencies.kt",
    "chars": 1783,
    "preview": "object BuildPlugins {\n  // keep in sync with buildSrc/build.gradle.kts\n  val androidGradlePlugin = \"com.android.tools.bu"
  },
  {
    "path": "buildSrc/src/main/kotlin/richtext-kmp-library.gradle.kts",
    "chars": 1072,
    "preview": "import AndroidConfiguration.compileSdk\nimport AndroidConfiguration.minSdk\nimport AndroidConfiguration.targetSdk\nimport o"
  },
  {
    "path": "desktop-sample/build.gradle.kts",
    "chars": 684,
    "preview": "import org.jetbrains.compose.desktop.application.dsl.TargetFormat\n\nplugins {\n  kotlin(\"jvm\")\n  id(\"org.jetbrains.compose"
  },
  {
    "path": "desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/MarkdownSampleApp.kt",
    "chars": 17902,
    "preview": "package com.halilibo.richtext.desktop\n\nimport androidx.compose.foundation.LocalScrollbarStyle\nimport androidx.compose.fo"
  },
  {
    "path": "desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/RichTextSampleApp.kt",
    "chars": 11214,
    "preview": "package com.halilibo.richtext.desktop\n\nimport androidx.compose.animation.Animatable\nimport androidx.compose.animation.co"
  },
  {
    "path": "docs/index.md",
    "chars": 2039,
    "preview": "# Overview\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.halilibo.compose-richtext/richtext-ui.svg?label"
  },
  {
    "path": "docs/richtext-commonmark.md",
    "chars": 3806,
    "preview": "# Commonmark Markdown\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)]"
  },
  {
    "path": "docs/richtext-markdown.md",
    "chars": 2115,
    "preview": "# Markdown\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://de"
  },
  {
    "path": "docs/richtext-ui-material.md",
    "chars": 1108,
    "preview": "# Richtext UI Material\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)"
  },
  {
    "path": "docs/richtext-ui-material3.md",
    "chars": 1113,
    "preview": "# Richtext UI Material 3\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badg"
  },
  {
    "path": "docs/richtext-ui.md",
    "chars": 4085,
    "preview": "# Richtext UI\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https:/"
  },
  {
    "path": "gen_dokka_docs.sh",
    "chars": 492,
    "preview": "#!/bin/bash\n\n# Fail on any error\nset -ex\n\nDOCS_ROOT=docs-gen\n\n[ -d $DOCS_ROOT ] && rm -r $DOCS_ROOT\nmkdir $DOCS_ROOT\n\n# "
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 252,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 2477,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 8669,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2868,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "mkdocs.yml",
    "chars": 858,
    "preview": "site_name: Compose Richtext\nrepo_name: compose-richtext\nrepo_url: https://github.com/halilozercan/compose-richtext\nsite_"
  },
  {
    "path": "richtext-commonmark/build.gradle.kts",
    "chars": 1010,
    "preview": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n\n  android {\n    namespace = \"com.halilib"
  },
  {
    "path": "richtext-commonmark/gradle.properties",
    "chars": 126,
    "preview": "POM_NAME=Compose Richtext Commonmark\nPOM_DESCRIPTION=A library for rendering markdown in Compose using the Commonmark li"
  },
  {
    "path": "richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/CommonMarkdownParseOptions.kt",
    "chars": 901,
    "preview": "package com.halilibo.richtext.commonmark\n\n/**\n * Allows configuration of the Markdown parser\n *\n * @param autolink Detec"
  },
  {
    "path": "richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt",
    "chars": 1921,
    "preview": "package com.halilibo.richtext.commonmark\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.get"
  },
  {
    "path": "richtext-commonmark/src/jvmAndroidMain/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt",
    "chars": 8692,
    "preview": "package com.halilibo.richtext.commonmark\n\nimport com.halilibo.richtext.markdown.node.AstBlockQuote\nimport com.halilibo.r"
  },
  {
    "path": "richtext-commonmark/src/jvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt",
    "chars": 6933,
    "preview": "package com.halilibo.richtext.markdown\n\nimport com.halilibo.richtext.commonmark.CommonMarkdownParseOptions\nimport com.ha"
  },
  {
    "path": "richtext-markdown/build.gradle.kts",
    "chars": 760,
    "preview": "plugins {\n  id(\"richtext-kmp-library\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo.richtext.markdown\"\n  }\n  so"
  },
  {
    "path": "richtext-markdown/gradle.properties",
    "chars": 117,
    "preview": "POM_NAME=Compose Richtext Markdown\nPOM_DESCRIPTION=A library for rendering markdown represented as an AST in Compose."
  },
  {
    "path": "richtext-markdown/src/androidMain/AndroidManifest.xml",
    "chars": 12,
    "preview": "<manifest />"
  },
  {
    "path": "richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "chars": 628,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remem"
  },
  {
    "path": "richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/MarkdownImage.kt",
    "chars": 2904,
    "preview": "package com.halilibo.richtext.markdown\n\nimport android.annotation.SuppressLint\nimport android.util.Base64\nimport android"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/BasicMarkdown.kt",
    "chars": 9093,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.runtim"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "chars": 343,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport com.halilibo.richtext.ui.RichT"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownImage.kt",
    "chars": 522,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\ni"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt",
    "chars": 6229,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.r"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt",
    "chars": 1207,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport com.halilibo.richtext.markdown"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt",
    "chars": 1639,
    "preview": "package com.halilibo.richtext.markdown\n\nimport com.halilibo.richtext.markdown.node.AstCode\nimport com.halilibo.richtext."
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt",
    "chars": 732,
    "preview": "package com.halilibo.richtext.markdown.node\n\n/**\n * Generic AstNode implementation that can define any node in Abstract "
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt",
    "chars": 1088,
    "preview": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\n\n/**\n * All the pointers that can"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt",
    "chars": 4080,
    "preview": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\nimport com.halilibo.richtext.ui.s"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt",
    "chars": 569,
    "preview": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\npublic object AstTabl"
  },
  {
    "path": "richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "chars": 447,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.runtim"
  },
  {
    "path": "richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt",
    "chars": 2068,
    "preview": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.runtime.Composa"
  },
  {
    "path": "richtext-ui/build.gradle.kts",
    "chars": 550,
    "preview": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo"
  },
  {
    "path": "richtext-ui/gradle.properties",
    "chars": 107,
    "preview": "POM_NAME=Compose Richtext UI\nPOM_DESCRIPTION=A library for rendering high-level text formatting in Compose."
  },
  {
    "path": "richtext-ui/src/androidMain/AndroidManifest.xml",
    "chars": 12,
    "preview": "<manifest />"
  },
  {
    "path": "richtext-ui/src/androidMain/kotlin/com/halilibo/richtext/ui/CodeBlock.android.kt",
    "chars": 521,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BasicRichText.kt",
    "chars": 961,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.f"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BlockQuote.kt",
    "chars": 3727,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.f"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/CodeBlock.kt",
    "chars": 3331,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layou"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/FormattedList.kt",
    "chars": 9332,
    "preview": "@file:Suppress(\"ComposableNaming\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimpo"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Heading.kt",
    "chars": 3029,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.r"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/HorizontalRule.kt",
    "chars": 885,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layou"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/InfoPanel.kt",
    "chars": 3526,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.borde"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextLocals.kt",
    "chars": 3127,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.foundation.t"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextScope.kt",
    "chars": 564,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.r"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextStyle.kt",
    "chars": 3441,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Composition"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeConfiguration.kt",
    "chars": 2273,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Composition"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeProvider.kt",
    "chars": 2349,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Composition"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/SimpleTableLayout.kt",
    "chars": 3653,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composab"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Table.kt",
    "chars": 5810,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.f"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/InlineContent.kt",
    "chars": 4025,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"FunctionName\")\n\npackage com.halilibo.richtext.ui.string\n\nim"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/RichTextString.kt",
    "chars": 15837,
    "preview": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"SuspiciousCollectionReassignment\")\n\npackage com.halilibo.ri"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/Text.kt",
    "chars": 2336,
    "preview": "package com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.co"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/ConditionalTapGestureDetector.kt",
    "chars": 9115,
    "preview": "@file:OptIn(ExperimentalComposeUiApi::class)\n\npackage com.halilibo.richtext.ui.util\n\nimport androidx.compose.foundation."
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/UUID.kt",
    "chars": 79,
    "preview": "package com.halilibo.richtext.ui.util\n\ninternal expect fun randomUUID(): String"
  },
  {
    "path": "richtext-ui/src/jvmAndroidMain/kotlin/com/halilibo/richtext/ui/util/UUID.kt",
    "chars": 144,
    "preview": "package com.halilibo.richtext.ui.util\n\nimport java.util.UUID\n\ninternal actual fun randomUUID(): String {\n  return UUID.r"
  },
  {
    "path": "richtext-ui/src/jvmMain/kotlin/com/halilibo/richtext/ui/CodeBlock.desktop.kt",
    "chars": 1331,
    "preview": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.HorizontalScrollbar\nimport androidx.compose.foundat"
  },
  {
    "path": "richtext-ui-material/build.gradle.kts",
    "chars": 485,
    "preview": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo"
  },
  {
    "path": "richtext-ui-material/gradle.properties",
    "chars": 125,
    "preview": "POM_NAME=Compose Richtext UI Material\nPOM_DESCRIPTION=An extension library for RichText UI to easily bind with Material "
  },
  {
    "path": "richtext-ui-material/src/androidMain/AndroidManifest.xml",
    "chars": 12,
    "preview": "<manifest />"
  },
  {
    "path": "richtext-ui-material/src/commonMain/kotlin/com/halilibo/richtext/ui/material/RichText.kt",
    "chars": 2170,
    "preview": "package com.halilibo.richtext.ui.material\n\nimport androidx.compose.material.LocalContentColor\nimport androidx.compose.ma"
  },
  {
    "path": "richtext-ui-material3/build.gradle.kts",
    "chars": 488,
    "preview": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo"
  },
  {
    "path": "richtext-ui-material3/gradle.properties",
    "chars": 127,
    "preview": "POM_NAME=Compose Richtext UI Material3\nPOM_DESCRIPTION=An extension library for RichText UI to easily bind with Material"
  },
  {
    "path": "richtext-ui-material3/src/androidMain/AndroidManifest.xml",
    "chars": 12,
    "preview": "<manifest />"
  },
  {
    "path": "richtext-ui-material3/src/commonMain/kotlin/com/halilibo/richtext/ui/material3/RichText.kt",
    "chars": 2175,
    "preview": "package com.halilibo.richtext.ui.material3\n\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose."
  },
  {
    "path": "settings.gradle.kts",
    "chars": 345,
    "preview": "pluginManagement {\n  repositories {\n    google()\n    gradlePluginPortal()\n    mavenCentral()\n  }\n}\n\ninclude(\":richtext-u"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the zach-klippenstein/compose-richtext GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 105 files (298.4 KB), approximately 79.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!