[
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Android CI\n\non:\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: gradle/wrapper-validation-action@v1\n      - name: set up JDK 21\n        uses: actions/setup-java@v1\n        with:\n          java-version: 21\n      - uses: actions/cache@v4\n        with:\n          path: ~/.gradle/caches\n          key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }}\n          restore-keys: gradle-${{ runner.os }}-\n      - run: ./gradlew build\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Doc Site\n\non:\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-java@v1\n        with:\n          java-version: 21\n      - uses: actions/setup-python@v2\n        with:\n          python-version: 3.x\n      - name: Install dependencies\n        run: pip install mkdocs-material\n      - name: Generate docs\n        run: ./gen_dokka_docs.sh\n      - run: mkdocs gh-deploy --force\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\n\non:\n  workflow_dispatch:\n\njobs:\n  publish:\n    name: Release build and publish\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: gradle/wrapper-validation-action@v1\n      - name: set up JDK 21\n        uses: actions/setup-java@v1\n        with:\n          java-version: 21\n      - uses: actions/cache@v4\n        with:\n          path: ~/.gradle/caches\n          key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }}\n          restore-keys: gradle-${{ runner.os }}-\n      - name: Release build\n        run: ./gradlew :build\n      - name: Publish to MavenCentral\n        run: ./gradlew publishAllPublicationsToMavenCentralRepository\n        env:\n          GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}\n          GPG_PRIVATE_PASSWORD: ${{ secrets.GPG_PRIVATE_PASSWORD }}\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\nbuild/\n/captures\n.externalNativeBuild\n.cxx\nsite/\ndocs-gen/\n"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <option name=\"OTHER_INDENT_OPTIONS\">\n      <value>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </value>\n    </option>\n    <option name=\"CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"999\" />\n    <option name=\"NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"999\" />\n    <option name=\"PACKAGES_TO_USE_IMPORT_ON_DEMAND\">\n      <value />\n    </option>\n    <option name=\"IMPORT_LAYOUT_TABLE\">\n      <value>\n        <package name=\"\" withSubpackages=\"true\" static=\"false\" />\n        <emptyLine />\n        <package name=\"\" withSubpackages=\"true\" static=\"true\" />\n      </value>\n    </option>\n    <option name=\"KEEP_BLANK_LINES_IN_DECLARATIONS\" value=\"1\" />\n    <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n    <option name=\"LINE_COMMENT_AT_FIRST_COLUMN\" value=\"false\" />\n    <option name=\"BLOCK_COMMENT_AT_FIRST_COLUMN\" value=\"false\" />\n    <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n    <option name=\"KEEP_BLANK_LINES_BEFORE_RBRACE\" value=\"0\" />\n    <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n    <option name=\"ALIGN_MULTILINE_FOR\" value=\"false\" />\n    <option name=\"SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE\" value=\"true\" />\n    <option name=\"CALL_PARAMETERS_WRAP\" value=\"1\" />\n    <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n    <option name=\"RESOURCE_LIST_WRAP\" value=\"1\" />\n    <option name=\"EXTENDS_LIST_WRAP\" value=\"1\" />\n    <option name=\"THROWS_LIST_WRAP\" value=\"1\" />\n    <option name=\"EXTENDS_KEYWORD_WRAP\" value=\"1\" />\n    <option name=\"THROWS_KEYWORD_WRAP\" value=\"1\" />\n    <option name=\"METHOD_CALL_CHAIN_WRAP\" value=\"5\" />\n    <option name=\"BINARY_OPERATION_WRAP\" value=\"5\" />\n    <option name=\"BINARY_OPERATION_SIGN_ON_NEXT_LINE\" value=\"true\" />\n    <option name=\"TERNARY_OPERATION_WRAP\" value=\"1\" />\n    <option name=\"TERNARY_OPERATION_SIGNS_ON_NEXT_LINE\" value=\"true\" />\n    <option name=\"FOR_STATEMENT_WRAP\" value=\"1\" />\n    <option name=\"ARRAY_INITIALIZER_WRAP\" value=\"1\" />\n    <option name=\"ASSIGNMENT_WRAP\" value=\"1\" />\n    <option name=\"WRAP_COMMENTS\" value=\"true\" />\n    <option name=\"ASSERT_STATEMENT_WRAP\" value=\"1\" />\n    <option name=\"IF_BRACE_FORCE\" value=\"1\" />\n    <option name=\"DOWHILE_BRACE_FORCE\" value=\"1\" />\n    <option name=\"WHILE_BRACE_FORCE\" value=\"1\" />\n    <option name=\"METHOD_ANNOTATION_WRAP\" value=\"1\" />\n    <option name=\"CLASS_ANNOTATION_WRAP\" value=\"1\" />\n    <option name=\"FIELD_ANNOTATION_WRAP\" value=\"1\" />\n    <option name=\"PARAMETER_ANNOTATION_WRAP\" value=\"1\" />\n    <option name=\"VARIABLE_ANNOTATION_WRAP\" value=\"1\" />\n    <option name=\"ENUM_CONSTANTS_WRAP\" value=\"1\" />\n    <AndroidXmlCodeStyleSettings>\n      <option name=\"LAYOUT_SETTINGS\">\n        <value>\n          <option name=\"INSERT_BLANK_LINE_BEFORE_TAG\" value=\"false\" />\n          <option name=\"INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE\" value=\"true\" />\n        </value>\n      </option>\n    </AndroidXmlCodeStyleSettings>\n    <GroovyCodeStyleSettings>\n      <option name=\"ALIGN_MULTILINE_LIST_OR_MAP\" value=\"false\" />\n      <option name=\"ALIGN_NAMED_ARGS_IN_MAP\" value=\"false\" />\n    </GroovyCodeStyleSettings>\n    <JavaCodeStyleSettings>\n      <option name=\"CLASS_NAMES_IN_JAVADOC\" value=\"3\" />\n      <option name=\"CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"999\" />\n      <option name=\"NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"999\" />\n      <option name=\"IMPORT_LAYOUT_TABLE\">\n        <value>\n          <package name=\"\" withSubpackages=\"true\" static=\"false\" />\n          <emptyLine />\n          <package name=\"\" withSubpackages=\"true\" static=\"true\" />\n        </value>\n      </option>\n      <option name=\"JD_ALIGN_PARAM_COMMENTS\" value=\"false\" />\n      <option name=\"JD_ALIGN_EXCEPTION_COMMENTS\" value=\"false\" />\n      <option name=\"JD_P_AT_EMPTY_LINES\" value=\"false\" />\n      <option name=\"JD_DO_NOT_WRAP_ONE_LINE_COMMENTS\" value=\"true\" />\n      <option name=\"JD_KEEP_EMPTY_PARAMETER\" value=\"false\" />\n      <option name=\"JD_KEEP_EMPTY_RETURN\" value=\"false\" />\n      <option name=\"JD_PRESERVE_LINE_FEEDS\" value=\"true\" />\n    </JavaCodeStyleSettings>\n    <JetCodeStyleSettings>\n      <option name=\"NAME_COUNT_TO_USE_STAR_IMPORT\" value=\"999\" />\n      <option name=\"NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS\" value=\"999\" />\n      <option name=\"IMPORT_NESTED_CLASSES\" value=\"true\" />\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </JetCodeStyleSettings>\n    <ADDITIONAL_INDENT_OPTIONS fileType=\"php\">\n      <option name=\"INDENT_SIZE\" value=\"2\" />\n      <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n      <option name=\"TAB_SIZE\" value=\"2\" />\n    </ADDITIONAL_INDENT_OPTIONS>\n    <ADDITIONAL_INDENT_OPTIONS fileType=\"scala\">\n      <option name=\"INDENT_SIZE\" value=\"2\" />\n      <option name=\"CONTINUATION_INDENT_SIZE\" value=\"2\" />\n      <option name=\"TAB_SIZE\" value=\"2\" />\n    </ADDITIONAL_INDENT_OPTIONS>\n    <codeStyleSettings language=\"CSS\">\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"CoffeeScript\">\n      <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n    </codeStyleSettings>\n    <codeStyleSettings language=\"Groovy\">\n      <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n      <option name=\"KEEP_BLANK_LINES_IN_DECLARATIONS\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_BEFORE_RBRACE\" value=\"0\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"ALIGN_MULTILINE_FOR\" value=\"false\" />\n      <option name=\"CALL_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_LIST_WRAP\" value=\"1\" />\n      <option name=\"THROWS_LIST_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_KEYWORD_WRAP\" value=\"1\" />\n      <option name=\"THROWS_KEYWORD_WRAP\" value=\"1\" />\n      <option name=\"METHOD_CALL_CHAIN_WRAP\" value=\"5\" />\n      <option name=\"BINARY_OPERATION_WRAP\" value=\"5\" />\n      <option name=\"TERNARY_OPERATION_WRAP\" value=\"1\" />\n      <option name=\"FOR_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"ASSIGNMENT_WRAP\" value=\"1\" />\n      <option name=\"ASSERT_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"IF_BRACE_FORCE\" value=\"1\" />\n      <option name=\"WHILE_BRACE_FORCE\" value=\"1\" />\n      <option name=\"METHOD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"CLASS_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"FIELD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"PARAMETER_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"VARIABLE_ANNOTATION_WRAP\" value=\"1\" />\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"JAVA\">\n      <option name=\"LINE_COMMENT_AT_FIRST_COLUMN\" value=\"false\" />\n      <option name=\"BLOCK_COMMENT_AT_FIRST_COLUMN\" value=\"false\" />\n      <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n      <option name=\"KEEP_BLANK_LINES_IN_DECLARATIONS\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_BEFORE_RBRACE\" value=\"0\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"ALIGN_MULTILINE_FOR\" value=\"false\" />\n      <option name=\"SPACE_WITHIN_ARRAY_INITIALIZER_BRACES\" value=\"true\" />\n      <option name=\"SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE\" value=\"true\" />\n      <option name=\"CALL_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"RESOURCE_LIST_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_LIST_WRAP\" value=\"1\" />\n      <option name=\"THROWS_LIST_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_KEYWORD_WRAP\" value=\"1\" />\n      <option name=\"THROWS_KEYWORD_WRAP\" value=\"1\" />\n      <option name=\"METHOD_CALL_CHAIN_WRAP\" value=\"5\" />\n      <option name=\"BINARY_OPERATION_WRAP\" value=\"5\" />\n      <option name=\"BINARY_OPERATION_SIGN_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"TERNARY_OPERATION_WRAP\" value=\"1\" />\n      <option name=\"TERNARY_OPERATION_SIGNS_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"FOR_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"ARRAY_INITIALIZER_WRAP\" value=\"1\" />\n      <option name=\"ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"ASSIGNMENT_WRAP\" value=\"1\" />\n      <option name=\"WRAP_COMMENTS\" value=\"true\" />\n      <option name=\"ASSERT_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"IF_BRACE_FORCE\" value=\"1\" />\n      <option name=\"DOWHILE_BRACE_FORCE\" value=\"1\" />\n      <option name=\"WHILE_BRACE_FORCE\" value=\"1\" />\n      <option name=\"METHOD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"CLASS_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"FIELD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"PARAMETER_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"VARIABLE_ANNOTATION_WRAP\" value=\"1\" />\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"JSON\">\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n    </codeStyleSettings>\n    <codeStyleSettings language=\"JavaScript\">\n      <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"ALIGN_MULTILINE_FOR\" value=\"false\" />\n      <option name=\"CALL_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"BINARY_OPERATION_WRAP\" value=\"5\" />\n      <option name=\"BINARY_OPERATION_SIGN_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"TERNARY_OPERATION_WRAP\" value=\"1\" />\n      <option name=\"TERNARY_OPERATION_SIGNS_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"FOR_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"ARRAY_INITIALIZER_WRAP\" value=\"1\" />\n      <option name=\"ASSIGNMENT_WRAP\" value=\"1\" />\n      <option name=\"IF_BRACE_FORCE\" value=\"1\" />\n      <option name=\"DOWHILE_BRACE_FORCE\" value=\"1\" />\n      <option name=\"WHILE_BRACE_FORCE\" value=\"1\" />\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"SQL\">\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n    </codeStyleSettings>\n    <codeStyleSettings language=\"TypeScript\">\n      <option name=\"KEEP_FIRST_COLUMN_COMMENT\" value=\"false\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"ALIGN_MULTILINE_FOR\" value=\"false\" />\n      <option name=\"CALL_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"METHOD_PARAMETERS_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_LIST_WRAP\" value=\"1\" />\n      <option name=\"EXTENDS_KEYWORD_WRAP\" value=\"1\" />\n      <option name=\"BINARY_OPERATION_WRAP\" value=\"5\" />\n      <option name=\"BINARY_OPERATION_SIGN_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"TERNARY_OPERATION_WRAP\" value=\"1\" />\n      <option name=\"TERNARY_OPERATION_SIGNS_ON_NEXT_LINE\" value=\"true\" />\n      <option name=\"FOR_STATEMENT_WRAP\" value=\"1\" />\n      <option name=\"ARRAY_INITIALIZER_WRAP\" value=\"1\" />\n      <option name=\"ASSIGNMENT_WRAP\" value=\"1\" />\n      <option name=\"WRAP_COMMENTS\" value=\"true\" />\n      <option name=\"IF_BRACE_FORCE\" value=\"1\" />\n      <option name=\"DOWHILE_BRACE_FORCE\" value=\"1\" />\n      <option name=\"WHILE_BRACE_FORCE\" value=\"1\" />\n    </codeStyleSettings>\n    <codeStyleSettings language=\"XML\">\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n      <arrangement>\n        <rules>\n          <section>\n            <rule>\n              <match>\n                <NAME>class</NAME>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <NAME>layout</NAME>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <NAME>xmlns:android</NAME>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <NAME>xmlns:.*</NAME>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:id</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:name</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:layout_width</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:layout_height</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:layout_.*</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>app:layout_.*</NAME>\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <NAME>.*(?&lt;!style)$</NAME>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <NAME>style</NAME>\n              </match>\n            </rule>\n          </section>\n        </rules>\n      </arrangement>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"kotlin\">\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n      <option name=\"KEEP_BLANK_LINES_IN_DECLARATIONS\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n      <option name=\"KEEP_BLANK_LINES_BEFORE_RBRACE\" value=\"1\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"METHOD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"FIELD_ANNOTATION_WRAP\" value=\"1\" />\n      <option name=\"ENUM_CONSTANTS_WRAP\" value=\"2\" />\n      <indentOptions>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </indentOptions>\n    </codeStyleSettings>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n  </state>\n</component>\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Changelog\n=========\n\n1.0.0-alpha03\n-------\n\nThis release removes the `printing` and `slideshow` modules to focus on the core Markdown and RichText functionalities. It also adds support for inline base64 images.\n\n### Breaking Changes\n- 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.\n\n### New Features\n- **Inline Base64 Image Rendering**: Markdown images can now be rendered from inline base64-encoded data URIs.\n\n### Updates & Maintenance\n- **Dependencies Updated**:\n    - Compose Multiplatform updated to `1.8.2`.\n    - Commonmark updated to `0.25.0`.\n    - Dokka updated to `2.0.0`.\n- **Build & CI**:\n    - Android Gradle Plugin and other dependencies have been updated.\n    - CI now uses `actions/cache@v4`.\n- **Sample App**:\n    - The Android sample app has been updated to reflect the removal of the `printing` and `slideshow` modules.\n    - Theme handling in the sample app has been simplified.\n\nv0.11.0\n------\n\n_2022_02_09_\n\n* Upgrade Coil to 2.0.0-alpha06 by @msfjarvis in https://github.com/halilozercan/compose-richtext/pull/72\n\n## New Contributors\n* @msfjarvis made their first contribution in https://github.com/halilozercan/compose-richtext/pull/72\n\n**Full Changelog**: https://github.com/halilozercan/compose-richtext/compare/v0.10.0...v0.11.0\n\nv0.10.0\n------\n\n_2021_12_05_\n\nThis release celebrates the release of Compose Multiplatform 1.0.0 🎉🥳\n\nv0.9.0\n------\n\n_2021_11_20_\n\nThis release is mostly a version bump.\n- Jetpack Compose: 1.1.0-beta03\n- Jetbrains Compose: 1.0.0-beta5\n- Kotlin: 1.5.31\n\nOther changes:\n\n* Fix link formatting in index page of docs by in https://github.com/halilozercan/compose-richtext/pull/60\n* CodeBlock fixes in https://github.com/halilozercan/compose-richtext/pull/62\n* Update CHANGELOG.md to include releases after the transfer in https://github.com/halilozercan/compose-richtext/pull/64\n* Add info panels similar to bootstrap alerts #54 in https://github.com/halilozercan/compose-richtext/pull/63\n\n\n**Full Changelog**: https://github.com/halilozercan/compose-richtext/compare/v0.8.1...v0.9.0\n\nv0.8.1\n------\n\n_2021-9-11_\n\nThis release fixes JVM artifact issue #59\n\nv0.8.0\n------\n\n_2021-9-8_\n\nCompose Richtext goes KMP, opening RichText UI and its extensions to both Android and Desktop (#50)\n\nSpecial thanks @zach-klippenstein @LouisCAD @russhwolf for their reviews and help.\n\n* Richtext UI, Richtext UI Material, and RichText Commonmark are now KMP Compose libraries\n* Slideshow, Printing remains Android only for the foreseeable future\n* Updated docs\n* A new CI compatible release configuration\n\nv0.7.0\n------\n\n_2021-8-6_\n\n* RichText UI no longer depends on Material (#45)\n* A new artifact richtext-ui-material is published to easily integrate RichText for apps that use Material design.\n* Upgraded compose to 1.0.1 and kotlin to 1.5.21\n\nv0.6.0\n------\n\n_2021-7-29_\n\n* **Compose 1.0.0 support** (#43)\n* Upgrade to Gradle 7.0.2 (#40)\n* Fix wrong word used. portrait -> landscape (#37 - thanks @LouisCAD)\n* Repository has migrated from @zach-klippenstein to @halilozercan.\n* Artifacts have moved from com.zachklipp.compose-richtext to com.halilibo.compose-richtext.\n* Similarly, documentation is also now available at halilibo.com/compose-richtext\n\nv0.5.0\n------\n\n_2021-5-18_\n\n* **Compose Beta 7 support!** (#36)\n* Fix several bugs in Table, RichTextStyle and improve InlineContent (#35 – thanks @halilozercan!)\n\nv0.2.0\n------\n\n_2021-2-27_\n\n* **Compose Beta 1 support!**\n* Remove BulletList styling for different leading characters - Update markdown-demo.png to show new\n  BulletList rendering (#28 – thanks @halilozercan!)\n\nv0.1.0+alpha06\n--------------\n\n_2020-11-06_\n\n* Initial release.\n\nThanks to @halilozercan for implementing Markdown support!"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Compose Markdown and Rich Text\n\n[![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)\n[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)\n\n> **Warning**\n> 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.\n\nA collection of Compose libraries for working with Markdown rendering and rich text formatting.\n\nAll modules are Compose Multiplatform compatible but lacks iOS support.\n\n----\n\n**Documentation is available at [halilibo.com/compose-richtext](https://halilibo.com/compose-richtext).**\n\n----\n\n```kotlin\n@Composable fun App() {\n  RichText(Modifier.background(color = Color.LightGray)) {\n    Heading(0, \"Title\")\n    Text(\"Summary paragraph.\")\n\n    HorizontalRule()\n\n    BlockQuote {\n      Text(\"A wise person once said…\")\n    }\n    \n    Markdown(\"**Hello** `World`\")\n  }\n}\n```\n\n## License\n```\nCopyright 2025 Halil Ozercan\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n"
  },
  {
    "path": "android-sample/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n  id(\"com.android.application\")\n  kotlin(\"android\")\n  id(\"org.jetbrains.compose\")\n  id(\"org.jetbrains.kotlin.plugin.compose\")\n}\n\nandroid {\n  namespace = \"com.zachklipp.richtext.sample\"\n  compileSdk = AndroidConfiguration.compileSdk\n\n  defaultConfig {\n    minSdk = AndroidConfiguration.minSdk\n    targetSdk = AndroidConfiguration.targetSdk\n  }\n\n  buildFeatures {\n    compose = true\n  }\n\n  compileOptions {\n    sourceCompatibility = JavaVersion.VERSION_11\n    targetCompatibility = JavaVersion.VERSION_11\n  }\n}\n\nkotlin {\n  compilerOptions {\n    jvmTarget = JvmTarget.JVM_11\n  }\n}\n\ndependencies {\n  implementation(project(\":richtext-commonmark\"))\n  implementation(project(\":richtext-ui-material3\"))\n  implementation(AndroidX.appcompat)\n  implementation(Compose.activity)\n  implementation(compose.foundation)\n  implementation(compose.materialIconsExtended)\n  implementation(compose.material3)\n  implementation(compose.uiTooling)\n}\n"
  },
  {
    "path": "android-sample/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "android-sample/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n\n  <application\n      android:allowBackup=\"false\"\n      android:icon=\"@mipmap/ic_launcher\"\n      android:label=\"@string/app_name\"\n      android:roundIcon=\"@mipmap/ic_launcher_round\"\n      android:supportsRtl=\"true\"\n      android:theme=\"@style/AppTheme\">\n\n    <activity\n        android:name=\".MainActivity\"\n        android:configChanges=\"orientation|screenSize|smallestScreenSize|fontScale|density|layoutDirection\"\n        android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n    </activity>\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.zachklipp.richtext.sample\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.halilibo.richtext.ui.BlockQuote\nimport com.halilibo.richtext.ui.CodeBlock\nimport com.halilibo.richtext.ui.FormattedList\nimport com.halilibo.richtext.ui.Heading\nimport com.halilibo.richtext.ui.HorizontalRule\nimport com.halilibo.richtext.ui.InfoPanel\nimport com.halilibo.richtext.ui.InfoPanelType\nimport com.halilibo.richtext.ui.ListType\nimport com.halilibo.richtext.ui.ListType.Ordered\nimport com.halilibo.richtext.ui.ListType.Unordered\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.Table\nimport com.halilibo.richtext.ui.material3.RichText\n\n@Preview(widthDp = 300, heightDp = 1000)\n@Composable fun RichTextDemoOnWhite() {\n  Box(Modifier.background(color = Color.White)) {\n    RichTextDemo()\n  }\n}\n\n@Preview(widthDp = 300, heightDp = 1000)\n@Composable fun RichTextDemoOnBlack() {\n  CompositionLocalProvider(LocalContentColor provides Color.White) {\n    Box(Modifier.background(color = Color.Black)) {\n      RichTextDemo()\n    }\n  }\n}\n\n@Composable fun RichTextDemo(\n  style: RichTextStyle? = null,\n  header: String = \"\"\n) {\n  RichText(\n    modifier = Modifier.padding(8.dp),\n    style = style\n  ) {\n    Heading(0, \"Paragraphs $header\")\n    Text(\"Simple paragraph.\")\n    Text(\"Paragraph with\\nmultiple lines.\")\n    Text(\"Paragraph with really long line that should be getting wrapped.\")\n    TextPreview()\n\n    Heading(0, \"Lists\")\n    Heading(1, \"Unordered\")\n    ListDemo(listType = Unordered)\n    Heading(1, \"Ordered\")\n    ListDemo(listType = Ordered)\n\n    Heading(0, \"Horizontal Line\")\n    Text(\"Above line\")\n    HorizontalRule()\n    Text(\"Below line\")\n\n    Heading(0, \"Code Block\")\n    CodeBlock(\n      \"\"\"\n        {\n          \"Hello\": \"world!\"\n        }\n      \"\"\".trimIndent()\n    )\n\n    Heading(0, \"Block Quote\")\n    BlockQuote {\n      Text(\"These paragraphs are quoted.\")\n      Text(\"More text.\")\n      BlockQuote {\n        Text(\"Nested block quote.\")\n      }\n    }\n\n    Heading(0, \"Info Panel\")\n    InfoPanel(InfoPanelType.Primary, \"Only text primary info panel\")\n    InfoPanel(InfoPanelType.Success) {\n      Column {\n        Text(\"Successfully sent some data\")\n        HorizontalRule()\n        BlockQuote {\n          Text(\"This is a quote\")\n        }\n      }\n    }\n\n    Heading(0, \"Table\")\n    Table(\n      modifier = Modifier.fillMaxWidth(),\n      headerRow = {\n        cell { Text(\"Column 1\") }\n        cell { Text(\"Column 2\") }\n      }) {\n      row {\n        cell { Text(\"Hello\") }\n        cell {\n          CodeBlock(\"Foo bar\")\n        }\n      }\n      row {\n        cell {\n          BlockQuote {\n            Text(\"Stuff\")\n          }\n        }\n        cell { Text(\"Hello world this is a really long line that is going to wrap hopefully\") }\n      }\n    }\n  }\n}\n\n@Composable private fun RichTextScope.ListDemo(listType: ListType) {\n  FormattedList(listType,\n    @Composable {\n      Text(\"First list item\")\n      FormattedList(listType,\n        @Composable { Text(\"Indented 1\") }\n      )\n    },\n    @Composable {\n      Text(\"Second list item.\")\n      FormattedList(listType,\n        @Composable { Text(\"Indented 2\") }\n      )\n    }\n  )\n}\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/LazyMarkdownSample.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport android.widget.Toast\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.halilibo.richtext.commonmark.CommonMarkdownParseOptions\nimport com.halilibo.richtext.commonmark.CommonmarkAstNodeParser\nimport com.halilibo.richtext.markdown.BasicMarkdown\nimport com.halilibo.richtext.markdown.node.AstDocument\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.currentRichTextStyle\nimport com.halilibo.richtext.ui.material3.RichText\nimport com.halilibo.richtext.ui.resolveDefaults\n\n@Preview\n@Composable private fun LazyMarkdownSamplePreview() {\n  LazyMarkdownSample()\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable fun LazyMarkdownSample() {\n  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }\n  var isDarkModeEnabled by remember { mutableStateOf(false) }\n  var isWordWrapEnabled by remember { mutableStateOf(true) }\n  var markdownParseOptions by remember { mutableStateOf(CommonMarkdownParseOptions.Default) }\n  var isAutolinkEnabled by remember { mutableStateOf(true) }\n\n  LaunchedEffect(isWordWrapEnabled) {\n    richTextStyle = richTextStyle.copy(\n      codeBlockStyle = richTextStyle.codeBlockStyle!!.copy(\n        wordWrap = isWordWrapEnabled\n      )\n    )\n  }\n  LaunchedEffect(isAutolinkEnabled) {\n    markdownParseOptions = markdownParseOptions.copy(\n      autolink = isAutolinkEnabled\n    )\n  }\n\n  val context = LocalContext.current\n\n  SampleTheme(isDarkModeEnabled) {\n    Surface {\n      Column {\n        // Config\n        Card(elevation = CardDefaults.elevatedCardElevation()) {\n          Column {\n            FlowRow {\n              CheckboxPreference(\n                onClick = {\n                  isDarkModeEnabled = !isDarkModeEnabled\n                },\n                checked = isDarkModeEnabled,\n                label = \"Dark Mode\"\n              )\n              CheckboxPreference(\n                onClick = {\n                  isWordWrapEnabled = !isWordWrapEnabled\n                },\n                checked = isWordWrapEnabled,\n                label = \"Word Wrap\"\n              )\n              CheckboxPreference(\n                onClick = {\n                  isAutolinkEnabled = !isAutolinkEnabled\n                },\n                checked = isAutolinkEnabled,\n                label = \"Autolink\"\n              )\n            }\n\n            RichTextStyleConfig(\n              richTextStyle = richTextStyle,\n              onChanged = { richTextStyle = it }\n            )\n          }\n        }\n\n        SelectionContainer {\n          val parser = remember(markdownParseOptions) {\n            CommonmarkAstNodeParser(markdownParseOptions)\n          }\n\n          val astNode = remember(parser) {\n            parser.parse(sampleMarkdown)\n          }\n\n          ProvideToastUriHandler(context) {\n            RichText(\n              style = richTextStyle,\n              modifier = Modifier.padding(8.dp),\n            ) {\n              LazyMarkdown(astNode)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * A function that renders Markdown content lazily at the top level. All markdown trees start with\n * an AstDocument. If a document is long enough, usually there are more than hundred child nodes\n * under the root. Then in turn rendering the whole content into the internal column of\n * `BasicRichText` becomes extremely inefficient. Instead, this renderer at least relieves the top\n * level rendering by turning the internal column into a LazyColumn. All other nodes below the\n * first level are rendered as usual.\n *\n * @param astNode Root node of Markdown tree. This can be obtained via a parser.\n */\n@Composable\nfun RichTextScope.LazyMarkdown(astNode: AstNode) {\n  require(astNode.type == AstDocument) {\n    \"Lazy Markdown rendering requires root level node to have a type of AstDocument.\"\n  }\n  // keep the same blockSpacing\n  val currentStyle = currentRichTextStyle\n  val resolvedStyle = remember(currentStyle) { currentStyle.resolveDefaults() }\n  val blockSpacing = with(LocalDensity.current) {\n    resolvedStyle.paragraphSpacing!!.toDp()\n  }\n  LazyColumn(verticalArrangement = Arrangement.spacedBy(blockSpacing)) {\n    var iter = astNode.links.firstChild\n    while (iter != null) {\n      // We need to store iter in a final variable because composition of `item` happens after\n      // iteration\n      val node = iter\n      item {\n        BasicMarkdown(node)\n      }\n      iter = iter.links.next\n    }\n  }\n}\n\n@Composable\nprivate fun CheckboxPreference(\n  onClick: () -> Unit,\n  checked: Boolean,\n  label: String\n) {\n  Row(\n    Modifier.clickable(onClick = onClick),\n    horizontalArrangement = Arrangement.spacedBy(8.dp),\n    verticalAlignment = Alignment.CenterVertically\n  ) {\n    Checkbox(\n      checked = checked,\n      onCheckedChange = { onClick() },\n    )\n    Text(label)\n  }\n}\n\nprivate val sampleMarkdown = \"\"\"\n  # Demo\n  Based on [this cheatsheet][cheatsheet]\n\n  ---\n\n  ## Headers\n  ---\n  # Header 1\n  ## Header 2\n  ### Header 3\n  #### Header 4\n  ##### Header 5\n  ###### Header 6\n  ---\n  \n  ## Full-bleed Image\n  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)\n\n  ## Images smaller than the width should center\n  ![](https://cdn.nostr.build/p/4a84.png)\n  \n  On LineHeight bug, the image below goes over this text. \n  ![](https://cdn.nostr.build/p/PxZ0.jpg)\n\n  ## Emphasis\n\n  Emphasis, aka italics, with *asterisks* or _underscores_.\n\n  Strong emphasis, aka bold, with **asterisks** or __underscores__.\n\n  Combined emphasis with **asterisks and _underscores_**.\n\n  ---\n\n  ## Lists\n  1. First ordered list item\n  2. Another item\n      * Unordered sub-list.\n  1. Actual numbers don't matter, just that it's a number\n      1. Ordered sub-list\n  4. And another item.\n\n      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).\n\n      To have a line break without a paragraph, you will need to use two trailing spaces.\n      Note that this line is separate, but within the same paragraph.\n      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)\n\n  * Unordered list can use asterisks\n  - Or minuses\n  + Or pluses\n<!-- -->\n  2. Ordered list starting with `2.`\n  3. Another item\n<!-- -->\n  0. Ordered list starting with `0.`\n<!-- -->\n  003. Ordered list starting with `003.`\n<!-- -->\n  -1. Starting with `-1.` should not be list\n\n\n  ---\n\n  ## Links\n\n  [I'm an inline-style link](https://www.google.com)\n\n  [I'm a reference-style link][Arbitrary case-insensitive reference text]\n\n  [I'm a relative reference to a repository file](../blob/master/LICENSE)\n\n  [You can use numbers for reference-style link definitions][1]\n\n  Or leave it empty and use the [link text itself].\n  \n  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.\n\n  ---\n\n  ## Code\n\n  Inline `code` has `back-ticks around` it.\n\n  ```javascript\n  var s = \"JavaScript syntax highlighting\";\n  alert(s);\n  ```\n\n  ```python\n  s = \"Python syntax highlighting\"\n  print s\n  ```\n\n  ```java\n  /**\n   * Helper method to obtain a Parser with registered strike-through &amp; table extensions\n   * &amp; task lists (added in 1.0.1)\n   *\n   * @return a Parser instance that is supported by this library\n   * @since 1.0.0\n   */\n  @NonNull\n  public static Parser createParser() {\n    return new Parser.Builder()\n        .extensions(Arrays.asList(\n            StrikethroughExtension.create(),\n            TablesExtension.create(),\n            TaskListExtension.create()\n        ))\n        .build();\n  }\n  ```\n\n  ```xml\n  <ScrollView\n    android:id=\"@+id/scroll_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginTop=\"?android:attr/actionBarSize\">\n\n    <TextView\n      android:id=\"@+id/text\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_margin=\"16dip\"\n      android:lineSpacingExtra=\"2dip\"\n      android:textSize=\"16sp\"\n      tools:text=\"yo\\nman\" />\n\n  </ScrollView>\n  ```\n\n  ```\n  No language indicated, so no syntax highlighting.\n  But let's throw in a <b>tag</b>.\n  ```\n  \n  ---\n\n  ## Images\n  \n  Inline-style:\n   \n  ![random image](https://picsum.photos/seed/picsum/400/400)\n  \n  ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  \n  Reference-style:\n   \n  ![random image][logo]\n  \n  [logo]: https://picsum.photos/seed/picsum2/400/400 \"Text 2\"\n\n  ---\n\n  ## Tables\n\n  Colons can be used to align columns.\n\n  | Tables        | Are           | Cool  |\n  | ------------- |:-------------:| -----:|\n  | col 3 is      | right-aligned | ${'$'}1600 |\n  | col 2 is      | centered      |   ${'$'}12 |\n  | zebra stripes | are neat      |    ${'$'}1 |\n\n  There must be at least 3 dashes separating each header cell.\n  The outer pipes (|) are optional, and you don't need to make the\n  raw Markdown line up prettily. You can also use inline Markdown.\n\n  Markdown | Less | Pretty\n  --- | --- | ---\n  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  1 | 2 | 3\n\n  ---\n\n  ## Blockquotes\n\n  > Blockquotes are very handy in email to emulate reply text.\n  > This line is part of the same quote.\n\n  Quote break.\n\n  > 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.\n\n  Nested quotes\n  > Hello!\n  >> And to you!\n\n  ---\n\n  ## Inline HTML\n\n  ```html\n  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>\n  ```\n\n  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>\n\n  ---\n\n  ## Horizontal Rule\n\n  Three or more...\n\n  ---\n\n  Hyphens (`-`)\n\n  ***\n\n  Asterisks (`*`)\n\n  ___\n\n  Underscores (`_`)\n\n\n  ## License\n\n  ```\n    Copyright 2019 Dimitry Ivanov (legal@noties.io)\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n        http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n  ```\n\n  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet\n\n  [arbitrary case-insensitive reference text]: https://www.mozilla.org\n  [1]: http://slashdot.org\n  [link text itself]: http://www.reddit.com\n\"\"\".trimIndent()"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport android.widget.Toast\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.commonmark.CommonMarkdownParseOptions\nimport com.halilibo.richtext.commonmark.CommonmarkAstNodeParser\nimport com.halilibo.richtext.markdown.AstBlockNodeComposer\nimport com.halilibo.richtext.markdown.BasicMarkdown\nimport com.halilibo.richtext.markdown.node.AstBlockNodeType\nimport com.halilibo.richtext.markdown.node.AstHeading\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.ui.Heading\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.material3.RichText\nimport com.halilibo.richtext.ui.resolveDefaults\n\n@Preview\n@Composable private fun MarkdownSamplePreview() {\n  MarkdownSample()\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable fun MarkdownSample() {\n  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }\n  var isWordWrapEnabled by remember { mutableStateOf(true) }\n  var markdownParseOptions by remember { mutableStateOf(CommonMarkdownParseOptions.Default) }\n  var isAutolinkEnabled by remember { mutableStateOf(true) }\n  var isRtl by remember { mutableStateOf(false) }\n\n  LaunchedEffect(isWordWrapEnabled) {\n    richTextStyle = richTextStyle.copy(\n      codeBlockStyle = richTextStyle.codeBlockStyle!!.copy(\n        wordWrap = isWordWrapEnabled\n      )\n    )\n  }\n  LaunchedEffect(isAutolinkEnabled) {\n    markdownParseOptions = markdownParseOptions.copy(\n      autolink = isAutolinkEnabled\n    )\n  }\n\n  val context = LocalContext.current\n\n  CompositionLocalProvider(\n    LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr\n  ) {\n    Column {\n      // Config\n      Card(elevation = CardDefaults.elevatedCardElevation()) {\n        Column {\n          FlowRow {\n            CheckboxPreference(\n              onClick = {\n                isWordWrapEnabled = !isWordWrapEnabled\n              },\n              checked = isWordWrapEnabled,\n              label = \"Word Wrap\"\n            )\n            CheckboxPreference(\n              onClick = {\n                isAutolinkEnabled = !isAutolinkEnabled\n              },\n              checked = isAutolinkEnabled,\n              label = \"Autolink\"\n            )\n            CheckboxPreference(\n              onClick = {\n                isRtl = !isRtl\n              },\n              checked = isRtl,\n              label = \"RTL Layout\"\n            )\n          }\n\n          RichTextStyleConfig(\n            richTextStyle = richTextStyle,\n            onChanged = { richTextStyle = it }\n          )\n        }\n      }\n\n      SelectionContainer {\n        Column(Modifier.verticalScroll(rememberScrollState())) {\n          val parser = remember(markdownParseOptions) {\n            CommonmarkAstNodeParser(markdownParseOptions)\n          }\n\n          val astNode = remember(parser) {\n            parser.parse(sampleMarkdown)\n          }\n\n          ProvideToastUriHandler(context) {\n            RichText(\n              style = richTextStyle,\n              modifier = Modifier.padding(8.dp),\n            ) {\n              BasicMarkdown(astNode, HeadingAstBlockNodeComposer)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nval HeadingAstBlockNodeComposer = object : AstBlockNodeComposer {\n  override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean {\n    return astBlockNodeType is AstHeading\n  }\n\n  @Composable override fun RichTextScope.Compose(\n    astNode: AstNode,\n    visitChildren: @Composable (AstNode) -> Unit\n  ) {\n    val headingNode = astNode.type as? AstHeading ?: return\n    Column {\n      Heading(level = headingNode.level) {\n        visitChildren(astNode)\n      }\n      Text(\"Custom rendering is used for this heading!\", fontSize = 8.sp)\n    }\n  }\n}\n\n@Composable\nprivate fun CheckboxPreference(\n  onClick: () -> Unit,\n  checked: Boolean,\n  label: String\n) {\n  Row(\n    Modifier.clickable(onClick = onClick),\n    horizontalArrangement = Arrangement.spacedBy(8.dp),\n    verticalAlignment = Alignment.CenterVertically\n  ) {\n    Checkbox(\n      checked = checked,\n      onCheckedChange = { onClick() },\n    )\n    Text(label)\n  }\n}\n\nprivate val sampleMarkdown = \"\"\"\n  # Demo\n  Based on [this cheatsheet][cheatsheet]\n\n  ---\n\n  ## Headers\n  ---\n  # Header 1\n  ## Header 2\n  ### Header 3\n  #### Header 4\n  ##### Header 5\n  ###### Header 6\n  ---\n  \n  ## Full-bleed Image\n  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)\n\n  ## Images smaller than the width should center\n  ![](https://cdn.nostr.build/p/4a84.png)\n  \n  On LineHeight bug, the image below goes over this text. \n  ![](https://cdn.nostr.build/p/PxZ0.jpg)\n\n  ## Emphasis\n\n  Emphasis, aka italics, with *asterisks* or _underscores_.\n\n  Strong emphasis, aka bold, with **asterisks** or __underscores__.\n\n  Combined emphasis with **asterisks and _underscores_**.\n\n  ---\n\n  ## Lists\n  1. First ordered list item\n  2. Another item\n      * Unordered sub-list.\n  1. Actual numbers don't matter, just that it's a number\n      1. Ordered sub-list\n  4. And another item.\n\n      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).\n\n      To have a line break without a paragraph, you will need to use two trailing spaces.\n      Note that this line is separate, but within the same paragraph.\n      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)\n\n  * Unordered list can use asterisks\n  - Or minuses\n  + Or pluses\n<!-- -->\n  2. Ordered list starting with `2.`\n  3. Another item\n<!-- -->\n  0. Ordered list starting with `0.`\n<!-- -->\n  003. Ordered list starting with `003.`\n<!-- -->\n  -1. Starting with `-1.` should not be list\n\n\n  ---\n\n  ## Links\n\n  [I'm an inline-style link](https://www.google.com)\n\n  [I'm a reference-style link][Arbitrary case-insensitive reference text]\n\n  [I'm a relative reference to a repository file](../blob/master/LICENSE)\n\n  [You can use numbers for reference-style link definitions][1]\n\n  Or leave it empty and use the [link text itself].\n  \n  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.\n\n  ---\n\n  ## Code\n\n  Inline `code` has `back-ticks around` it.\n\n  ```javascript\n  var s = \"JavaScript syntax highlighting\";\n  alert(s);\n  ```\n\n  ```python\n  s = \"Python syntax highlighting\"\n  print s\n  ```\n\n  ```java\n  /**\n   * Helper method to obtain a Parser with registered strike-through &amp; table extensions\n   * &amp; task lists (added in 1.0.1)\n   *\n   * @return a Parser instance that is supported by this library\n   * @since 1.0.0\n   */\n  @NonNull\n  public static Parser createParser() {\n    return new Parser.Builder()\n        .extensions(Arrays.asList(\n            StrikethroughExtension.create(),\n            TablesExtension.create(),\n            TaskListExtension.create()\n        ))\n        .build();\n  }\n  ```\n\n  ```xml\n  <ScrollView\n    android:id=\"@+id/scroll_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginTop=\"?android:attr/actionBarSize\">\n\n    <TextView\n      android:id=\"@+id/text\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_margin=\"16dip\"\n      android:lineSpacingExtra=\"2dip\"\n      android:textSize=\"16sp\"\n      tools:text=\"yo\\nman\" />\n\n  </ScrollView>\n  ```\n\n  ```\n  No language indicated, so no syntax highlighting.\n  But let's throw in a <b>tag</b>.\n  ```\n  \n  ---\n\n  ## Images\n  \n  Inline-style:\n   \n  ![random image](https://picsum.photos/seed/picsum/400/400)\n  \n  ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  \n  Reference-style:\n   \n  ![random image][logo]\n  \n  [logo]: https://picsum.photos/seed/picsum2/400/400 \"Text 2\"\n  \n  Base64 Inline\n  \n  ![][image1]\n\n  ---\n\n  ## Tables\n\n  Colons can be used to align columns.\n\n  | Tables        | Are           | Cool  |\n  | ------------- |:-------------:| -----:|\n  | col 3 is      | right-aligned | ${'$'}1600 |\n  | col 2 is      | centered      |   ${'$'}12 |\n  | zebra stripes | are neat      |    ${'$'}1 |\n\n  There must be at least 3 dashes separating each header cell.\n  The outer pipes (|) are optional, and you don't need to make the\n  raw Markdown line up prettily. You can also use inline Markdown.\n\n  Markdown | Less | Pretty\n  --- | --- | ---\n  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  1 | 2 | 3\n\n  ---\n\n  ## Blockquotes\n\n  > Blockquotes are very handy in email to emulate reply text.\n  > This line is part of the same quote.\n\n  Quote break.\n\n  > 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.\n\n  Nested quotes\n  > Hello!\n  >> And to you!\n\n  ---\n\n  ## Inline HTML\n\n  ```html\n  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>\n  ```\n\n  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>\n\n  ---\n\n  ## Horizontal Rule\n\n  Three or more...\n\n  ---\n\n  Hyphens (`-`)\n\n  ***\n\n  Asterisks (`*`)\n\n  ___\n\n  Underscores (`_`)\n\n\n  ## License\n\n  ```\n    Copyright 2019 Dimitry Ivanov (legal@noties.io)\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n        http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n  ```\n\n  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet\n\n  [arbitrary case-insensitive reference text]: https://www.mozilla.org\n  [1]: http://slashdot.org\n  [link text itself]: http://www.reddit.com\n  \n  [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=>\n\"\"\".trimIndent()"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/RichTextSample.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport androidx.annotation.IntRange\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.resolveDefaults\n\n@Preview\n@Composable private fun RichTextSamplePreview() {\n  RichTextSample()\n}\n\n@Composable fun RichTextSample() {\n  var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }\n\n  Column {\n    // Config\n    Card(elevation = CardDefaults.elevatedCardElevation()) {\n      Column {\n        RichTextStyleConfig(\n          richTextStyle = richTextStyle,\n          onChanged = { richTextStyle = it }\n        )\n      }\n    }\n\n    SelectionContainer {\n      Column(Modifier.verticalScroll(rememberScrollState())) {\n        RichTextDemo(style = richTextStyle)\n      }\n    }\n  }\n}\n\n@Composable\nfun RichTextStyleConfig(\n  richTextStyle: RichTextStyle,\n  onChanged: (RichTextStyle) -> Unit\n) {\n  Text(\"Paragraph spacing: ${richTextStyle.paragraphSpacing}\")\n  SliderForHumans(\n    value = richTextStyle.paragraphSpacing!!.value,\n    valueRange = 0f..20f,\n    onValueChange = {\n      onChanged(richTextStyle.copy(paragraphSpacing = it.sp))\n    }\n  )\n\n  Text(\"Table cell padding: ${richTextStyle.tableStyle!!.cellPadding}\")\n  SliderForHumans(\n    value = richTextStyle.tableStyle!!.cellPadding!!.value,\n    valueRange = 0f..20f,\n    onValueChange = {\n      onChanged(\n        richTextStyle.copy(\n          tableStyle = richTextStyle.tableStyle!!.copy(\n            cellPadding = it.sp\n          )\n        )\n      )\n    }\n  )\n\n  Text(\"Table border width padding: ${richTextStyle.tableStyle!!.borderStrokeWidth!!}\")\n  SliderForHumans(\n    value = richTextStyle.tableStyle!!.borderStrokeWidth!!,\n    valueRange = 0f..20f,\n    onValueChange = {\n      onChanged(\n        richTextStyle.copy(\n          tableStyle = richTextStyle.tableStyle!!.copy(\n            borderStrokeWidth = it\n          )\n        )\n      )\n    }\n  )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SliderForHumans(\n  value: Float,\n  onValueChange: (Float) -> Unit,\n  modifier: Modifier = Modifier,\n  enabled: Boolean = true,\n  valueRange: ClosedFloatingPointRange<Float> = 0f..1f,\n  @IntRange(from = 0) steps: Int = 0,\n  onValueChangeFinished: (() -> Unit)? = null,\n  colors: SliderColors = SliderDefaults.colors(),\n  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n) {\n  Slider(\n    value = value,\n    onValueChange = onValueChange,\n    modifier = modifier,\n    enabled = enabled,\n    valueRange = valueRange,\n    steps = steps,\n    onValueChangeFinished = onValueChangeFinished,\n    colors = colors,\n    interactionSource = interactionSource,\n    thumb = {\n      SliderDefaults.Thumb(\n        interactionSource = interactionSource,\n        thumbSize = DpSize(4.dp, 20.dp)\n      )\n    }\n  )\n}"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleActivity.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport android.os.Bundle\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\n\nclass MainActivity : AppCompatActivity() {\n\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    enableEdgeToEdge()\n    setContent {\n      SampleLauncher()\n    }\n  }\n}\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleLauncher.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.Crossfade\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBarsPadding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.DarkMode\nimport androidx.compose.material.icons.filled.LightMode\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\nprivate val Samples = listOf<Pair<String, @Composable () -> Unit>>(\n  \"RichText Demo\" to @Composable { RichTextSample() },\n  \"Markdown Demo\" to @Composable { MarkdownSample() },\n  \"Lazy Markdown Demo\" to @Composable { LazyMarkdownSample() },\n)\n\n@Preview(showBackground = true)\n@Composable private fun SampleLauncherPreview() {\n  SamplesListScreen(isDarkTheme = true, onSampleClicked = {}, onThemeToggleClicked = {})\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable fun SampleLauncher() {\n  val systemDarkTheme = isSystemInDarkTheme()\n  var isDarkTheme by remember(systemDarkTheme) { mutableStateOf(systemDarkTheme) }\n  var currentSampleIndex: Int? by remember { mutableStateOf(null) }\n\n  SampleTheme(isDarkTheme) {\n    Crossfade(currentSampleIndex) { index ->\n      if (index != null) {\n        BackHandler(onBack = { currentSampleIndex = null })\n        Scaffold(\n          topBar = {\n            TopAppBar(title = { Text(Samples[index].first) }, actions = {\n              val icon = if (isDarkTheme) Icons.Filled.LightMode else Icons.Filled.DarkMode\n              IconButton(onClick = { isDarkTheme = !isDarkTheme }) {\n                Icon(icon, contentDescription = \"Change color scheme\")\n              }\n            })\n          }\n        ) {\n          Surface(Modifier.padding(it)) {\n            Samples[index].second()\n          }\n        }\n      } else {\n        SamplesListScreen(\n          isDarkTheme,\n          onSampleClicked = { currentSampleIndex = it },\n          onThemeToggleClicked = { isDarkTheme = !isDarkTheme }\n        )\n      }\n    }\n  }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable private fun SamplesListScreen(\n  isDarkTheme: Boolean,\n  onSampleClicked: (Int) -> Unit,\n  onThemeToggleClicked: () -> Unit,\n) {\n  Scaffold(\n    topBar = {\n      TopAppBar(title = { Text(\"Samples\") }, actions = {\n        val icon = if (isDarkTheme) Icons.Filled.LightMode else Icons.Filled.DarkMode\n        IconButton(onClick = onThemeToggleClicked) {\n          Icon(icon, contentDescription = \"Change color scheme\")\n        }\n      })\n    }\n  ) { contentPadding ->\n    LazyColumn(modifier = Modifier.padding(contentPadding)) {\n      itemsIndexed(Samples) { index, (title, sampleContent) ->\n        ListItem(\n          headlineContent = { Text(title) },\n          modifier = Modifier.clickable(onClick = { onSampleClicked(index) }),\n          leadingContent = { SamplePreview(sampleContent) }\n        )\n      }\n    }\n  }\n}\n\n@Composable private fun SamplePreview(content: @Composable () -> Unit) {\n  ScreenPreview(\n    Modifier\n      .size(50.dp)\n      .aspectRatio(1f)\n      .clipToBounds()\n      // \"Zoom in\" to the top-start corner to make the preview more legible.\n      .graphicsLayer(\n        scaleX = 1.5f, scaleY = 1.5f,\n        transformOrigin = TransformOrigin(0f, 0f)\n      ),\n  ) {\n    SampleTheme {\n      Surface(content = content)\n    }\n  }\n}\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/SampleTheme.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Shapes\nimport androidx.compose.material3.Typography\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.TextUnit\n\n@Composable\nfun SampleTheme(\n  isDarkTheme: Boolean = isSystemInDarkTheme(),\n  shapes: Shapes = MaterialTheme.shapes,\n  typography: Typography = MaterialTheme.typography,\n  content: @Composable () -> Unit\n) {\n  val supportsDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n\n  val lightColorScheme = lightColorScheme(primary = Color(0xFF1EB980))\n\n  val darkColorScheme = darkColorScheme(primary = Color(0xFF66ffc7))\n\n  val colorScheme =\n    when {\n      supportsDynamicColor && isDarkTheme -> {\n        dynamicDarkColorScheme(LocalContext.current)\n      }\n      supportsDynamicColor && !isDarkTheme -> {\n        dynamicLightColorScheme(LocalContext.current)\n      }\n      isDarkTheme -> darkColorScheme\n      else -> lightColorScheme\n    }\n  MaterialTheme(colorScheme, shapes, typography) {\n    val textStyle = LocalTextStyle.current.copy(lineHeight = TextUnit.Unspecified)\n    CompositionLocalProvider(LocalTextStyle provides textStyle) {\n      content()\n    }\n  }\n}"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/ScreenPreview.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage com.zachklipp.richtext.sample\n\nimport android.content.Context\nimport android.content.Context.DISPLAY_SERVICE\nimport android.content.Context.WINDOW_SERVICE\nimport android.hardware.display.DisplayManager\nimport android.hardware.display.DisplayManager.DisplayListener\nimport android.os.Handler\nimport android.os.Looper\nimport android.util.DisplayMetrics\nimport android.view.WindowManager\nimport android.widget.FrameLayout\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.text.selection.DisableSelection\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.RememberObserver\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.PointerEvent\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerEventPass.Initial\nimport androidx.compose.ui.input.pointer.PointerInputFilter\nimport androidx.compose.ui.input.pointer.PointerInputModifier\nimport androidx.compose.ui.input.pointer.consumeAllChanges\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.semantics.disabled\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.IntSize\n\n/**\n * Displays [content] according to the current layout constraints, but with the density adjusted so\n * that the content things it's rendering inside a full-size screen, where \"full-size\" is defined\n * by [screenSize]. The default [screenSize] is read from the current window's default display.\n */\n// TODO Disable focus\n// TODO Disable key events (maybe covered by focus?)\n// TODO use this for Slideshow as well.\n@Composable fun ScreenPreview(\n  modifier: Modifier = Modifier,\n  screenSize: IntSize = rememberDefaultDisplaySize(),\n  content: @Composable () -> Unit\n) {\n  val aspectRatio = screenSize.width.toFloat() / screenSize.height.toFloat()\n  BoxWithConstraints(\n    modifier\n      .aspectRatio(aspectRatio)\n      // Disable touch input.\n      .then(PassthroughTouchToParentModifier)\n      .semantics(mergeDescendants = true) {\n        // TODO Block semantics. Is this enough?\n        disabled()\n      }\n  ) {\n    val actualDensity = LocalDensity.current.density\n    // Can use width or height to do the calculation, since the aspect ratio is enforced.\n    val previewDensityScale = constraints.maxWidth / screenSize.width.toFloat()\n    val previewDensity = actualDensity * previewDensityScale\n\n    // Provide a fake host view, since the preview doesn't really belong to this host view.\n    val context = LocalContext.current\n    val previewView = remember {\n      val previewContext = context.applicationContext\n      FrameLayout(previewContext)\n    }\n\n    DisableSelection {\n      CompositionLocalProvider(\n        LocalDensity provides Density(previewDensity),\n        LocalView provides previewView,\n        content = content\n      )\n    }\n  }\n}\n\n/**\n * Returns the size of the default display for the window manager of the window this composable is\n * currently attached to. Will also recompose if the display size changes, e.g. when the device is\n * rotated.\n *\n * If the display reports an empty size (0x0), e.g. when running in a preview, then a reasonable\n * fake size of a phone display in portrait orientation is returned instead.\n */\n@Composable private fun rememberDefaultDisplaySize(): IntSize {\n  val context = LocalContext.current\n  val state = remember { DisplaySizeCalculator(context) }\n  return state.displaySize.value\n}\n\nprivate class DisplaySizeCalculator(context: Context) : RememberObserver,\n  DisplayListener {\n  private val windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager\n  private val displayManager = context.getSystemService(DISPLAY_SERVICE) as DisplayManager\n  private val display = windowManager.defaultDisplay\n\n  val displaySize = mutableStateOf(getDisplaySize())\n\n  override fun onAbandoned() {\n    // Noop\n  }\n\n  override fun onRemembered() {\n    // Update the preview on device rotation, for example.\n    displayManager.registerDisplayListener(this, Handler(Looper.getMainLooper()))\n  }\n\n  override fun onForgotten() {\n    displayManager.unregisterDisplayListener(this)\n  }\n\n  override fun onDisplayChanged(displayId: Int) {\n    if (displayId != display.displayId) return\n    displaySize.value = getDisplaySize()\n  }\n\n  override fun onDisplayAdded(displayId: Int) = Unit\n  override fun onDisplayRemoved(displayId: Int) = Unit\n\n  private fun getDisplaySize(): IntSize {\n    val metrics = DisplayMetrics().also(display::getMetrics)\n    return if (metrics.widthPixels != 0 && metrics.heightPixels != 0) {\n      IntSize(metrics.widthPixels, metrics.heightPixels)\n    } else {\n      // Zero-sized display? Probably in a preview. Return some fake reasonable default.\n      IntSize(1080, 1920)\n    }\n  }\n}\n\n/**\n * A [PointerInputModifier] that blocks all touch events to children of the composable to which it's\n * applied, and instead allows all those events to flow to any filters defined on the parent\n * composable.\n */\nprivate object PassthroughTouchToParentModifier : PointerInputModifier, PointerInputFilter() {\n  override val pointerInputFilter: PointerInputFilter get() = this\n\n  override fun onPointerEvent(\n    pointerEvent: PointerEvent,\n    pass: PointerEventPass,\n    bounds: IntSize\n  ) {\n    if (pass == Initial) {\n      // On the initial pass (ancestors -> descendants), mark all pointer events as completely\n      // consumed. This prevents children from handling any pointer events.\n      // These events are all marked as unconsumed by default.\n      pointerEvent.changes.forEach {\n        it.consumeAllChanges()\n      }\n    }\n  }\n\n  override fun onCancel() {\n    // Noop.\n  }\n}\n"
  },
  {
    "path": "android-sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt",
    "content": "package com.zachklipp.richtext.sample\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.compose.animation.Animatable\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.keyframes\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap.Companion.Round\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.em\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.material3.RichText\nimport com.halilibo.richtext.ui.string.InlineContent\nimport com.halilibo.richtext.ui.string.RichTextString.Builder\nimport com.halilibo.richtext.ui.string.RichTextString.Format\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Bold\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Code\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Italic\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Link\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Subscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Superscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Underline\nimport com.halilibo.richtext.ui.string.Text\nimport com.halilibo.richtext.ui.string.richTextString\nimport com.halilibo.richtext.ui.string.withFormat\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n@Preview(showBackground = true)\n@Composable fun TextPreview() {\n  val context = LocalContext.current\n  var toggleLink by remember { mutableStateOf(false) }\n  val text = remember(context, toggleLink) {\n    richTextString {\n      appendPreviewSentence(Bold)\n      appendPreviewSentence(Italic)\n      appendPreviewSentence(Underline)\n      appendPreviewSentence(Strikethrough)\n      appendPreviewSentence(Subscript)\n      appendPreviewSentence(Superscript)\n      appendPreviewSentence(Code)\n      appendPreviewSentence(\n        Link(\"\") { toggleLink = !toggleLink },\n        if (toggleLink) \"clicked link\" else \"link\"\n      )\n      append(\"Here, \")\n      appendInlineContent(content = spinningCross)\n      append(\", is an inline image. \")\n      append(\"And here, \")\n      appendInlineContent(content = slowLoadingImage)\n      append(\", is an inline image that loads after some delay.\")\n      append(\"\\n\\n\")\n\n      append(\"Here \")\n      withFormat(Underline) {\n        append(\"is a \")\n        withFormat(Italic) {\n          append(\"longer sentence \")\n          withFormat(Bold) {\n            append(\"with many \")\n            withFormat(Code) {\n              append(\"different \")\n              withFormat(Strikethrough) {\n                append(\"nested\")\n              }\n              append(\" \")\n            }\n          }\n          append(\"styles.\")\n        }\n      }\n    }\n  }\n  RichText {\n    Text(text)\n  }\n}\n\nprivate val spinningCross = InlineContent {\n  val angle = remember { Animatable(0f) }\n  val color = remember { Animatable(Color.Red) }\n  LaunchedEffect(Unit) {\n    val angleAnim = infiniteRepeatable<Float>(\n      animation = tween(durationMillis = 1000, easing = LinearEasing)\n    )\n    launch { angle.animateTo(360f, angleAnim) }\n\n    val colorAnim = infiniteRepeatable<Color>(\n      animation = keyframes {\n        durationMillis = 2500\n        Color.Blue at 500\n        Color.Cyan at 1000\n        Color.Green at 1500\n        Color.Magenta at 2000\n      }\n    )\n    launch { color.animateTo(Color.Yellow, colorAnim) }\n  }\n\n  Canvas(modifier = Modifier\n    .size(12.sp.toDp(), 12.sp.toDp())\n    .padding(2.dp)) {\n    withTransform({ rotate(angle.value, center) }) {\n      val strokeWidth = 3.dp.toPx()\n      val strokeCap = Round\n      drawLine(\n        color.value,\n        start = Offset(0f, size.height / 2),\n        end = Offset(size.width, size.height / 2),\n        strokeWidth = strokeWidth,\n        cap = strokeCap\n      )\n      drawLine(\n        color.value,\n        start = Offset(size.width / 2, 0f),\n        end = Offset(size.width / 2, size.height),\n        strokeWidth = strokeWidth,\n        cap = strokeCap\n      )\n    }\n  }\n}\n\nval slowLoadingImage = InlineContent {\n  var loaded by rememberSaveable { mutableStateOf(false) }\n  LaunchedEffect(loaded) {\n    if (!loaded) {\n      delay(3000)\n      loaded = true\n    }\n  }\n\n  if (!loaded) {\n    LoadingSpinner()\n  } else {\n    Box(Modifier.clickable(onClick = { loaded = false })) {\n      val size = remember { Animatable(16f) }\n      LaunchedEffect(Unit) { size.animateTo(100f) }\n      Picture(Modifier.size(size.value.sp.toDp()))\n      Text(\n        \"click to refresh\",\n        modifier = Modifier\n          .padding(3.dp)\n          .align(Alignment.Center),\n        fontSize = 8.sp,\n        style = TextStyle(background = Color.LightGray)\n      )\n    }\n  }\n}\n\n@Composable private fun LoadingSpinner() {\n  val alpha = remember { Animatable(1f) }\n  LaunchedEffect(Unit) {\n    val anim = infiniteRepeatable<Float>(\n      animation = keyframes {\n        durationMillis = 500\n        0f at 250\n        1f at 500\n      })\n    alpha.animateTo(0f, anim)\n  }\n  Text(\n    \"⏳\",\n    fontSize = 3.em,\n    modifier = Modifier\n      .wrapContentSize(Alignment.Center)\n      .graphicsLayer(alpha = alpha.value)\n  )\n}\n\n@Composable private fun Picture(modifier: Modifier) {\n  Canvas(modifier) {\n    drawRect(Color.LightGray)\n    drawLine(Color.Red, Offset(0f, 0f), Offset(size.width, size.height))\n    drawLine(Color.Red, Offset(0f, size.height), Offset(size.width, 0f))\n  }\n}\n\n@OptIn(ExperimentalStdlibApi::class)\nprivate fun Builder.appendPreviewSentence(\n  format: Format,\n  text: String = format.javaClass.simpleName.replaceFirstChar { it.lowercase() }\n) {\n  append(\"Here is some \")\n  withFormat(format) {\n    append(text)\n  }\n  append(\" text. \")\n}\n\n@Composable\nfun ProvideToastUriHandler(context: Context, content: @Composable () -> Unit) {\n  val uriHandler = remember(context) {\n    object : UriHandler {\n      override fun openUri(uri: String) {\n        Toast.makeText(context, uri, Toast.LENGTH_SHORT).show()\n      }\n    }\n  }\n\n  CompositionLocalProvider(LocalUriHandler provides uriHandler, content)\n}\n"
  },
  {
    "path": "android-sample/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n  <path\n      android:fillColor=\"#3DDC84\"\n      android:pathData=\"M0,0h108v108h-108z\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M9,0L9,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,0L19,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M29,0L29,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M39,0L39,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M49,0L49,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M59,0L59,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M69,0L69,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M79,0L79,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M89,0L89,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M99,0L99,108\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,9L108,9\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,19L108,19\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,29L108,29\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,39L108,39\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,49L108,49\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,59L108,59\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,69L108,69\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,79L108,79\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,89L108,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,99L108,99\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,29L89,29\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,39L89,39\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,49L89,49\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,59L89,59\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,69L89,69\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,79L89,79\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M29,19L29,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M39,19L39,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M49,19L49,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M59,19L59,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M69,19L69,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M79,19L79,89\"\n      android:strokeWidth=\"0.8\"\n      android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "android-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n  <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\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient\n          android:endX=\"85.84757\"\n          android:endY=\"92.4963\"\n          android:startX=\"42.9492\"\n          android:startY=\"49.59793\"\n          android:type=\"linear\">\n        <item\n            android:color=\"#44000000\"\n            android:offset=\"0.0\" />\n        <item\n            android:color=\"#00000000\"\n            android:offset=\"1.0\" />\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"nonZero\"\n      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\"\n      android:strokeWidth=\"1\"\n      android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "android-sample/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#eeeeee</color>\n  <color name=\"colorPrimaryDark\">#111111</color>\n  <color name=\"colorAccent\">#2079c7</color>\n</resources>\n"
  },
  {
    "path": "android-sample/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string name=\"app_name\">Rich Text Sample</string>\n</resources>"
  },
  {
    "path": "android-sample/src/main/res/values/styles.xml",
    "content": "<resources>\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n    <!-- Customize your theme here. -->\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n  id(\"org.jetbrains.dokka\")\n}\n\nrepositories {\n  mavenCentral()\n}\n\ndokka {\n  dokkaPublications.configureEach {\n    outputDirectory.set(rootProject.file(\"docs/api\"))\n  }\n}\n\ndependencies {\n  dokka(project(\":richtext-ui\"))\n  dokka(project(\":richtext-ui-material\"))\n  dokka(project(\":richtext-ui-material3\"))\n  dokka(project(\":richtext-markdown\"))\n  dokka(project(\":richtext-commonmark\"))\n}\n\n// See https://stackoverflow.com/questions/25324880/detect-ide-environment-with-gradle\nfun isRunningFromIde(): Boolean {\n  return project.properties[\"android.injected.invoked.from.ide\"] == \"true\"\n}\n\nsubprojects {\n  repositories {\n    google()\n    mavenCentral()\n  }\n\n  tasks.withType<KotlinCompile>().all {\n    compilerOptions {\n//       TODO(stable); Disable warnings as errors until we get to 1.0.0\n//       Allow warnings when running from IDE, makes it easier to experiment.\n//      if (!isRunningFromIde()) {\n//        allWarningsAsErrors = true\n//      }\n\n      freeCompilerArgs = listOf(\"-opt-in=kotlin.RequiresOptIn\", \"-Xexpect-actual-classes\")\n    }\n  }\n\n  // taken from https://github.com/google/accompanist/blob/main/build.gradle\n  afterEvaluate {\n    if (tasks.findByName(\"dokkaHtmlPartial\") == null) {\n      // If dokka isn't enabled on this module, skip\n      return@afterEvaluate\n    }\n  }\n}\n\n//disable until the library reaches 1.0.0-beta01\n//apply plugin: 'binary-compatibility-validator'\n//apiValidation {\n//  // Ignore all sample projects, since they're not part of our API.\n//  // Only leaf project name is valid configuration, and every project must be individually ignored.\n//  // See https://github.com/Kotlin/binary-compatibility-validator/issues/3\n//  ignoredProjects += project('sample').name\n//  ignoredProjects += project('desktop').name\n//  ignoredProjects += project('richtext-ui-kmm').name\n//  ignoredProjects += project('richtext-commonmark-kmm').name\n//}\n"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "content": "repositories {\n  google()\n  mavenCentral()\n}\n\nplugins {\n  `kotlin-dsl`\n  `kotlin-dsl-precompiled-script-plugins`\n}\n\ndependencies {\n  // keep in sync with Dependencies.BuildPlugins.androidGradlePlugin\n  implementation(\"com.android.tools.build:gradle:9.1.0\")\n  implementation(\"com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.36.0\")\n  // keep in sync with Dependencies.Kotlin.gradlePlugin\n  implementation(\"org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10\")\n  // keep in sync with Dependencies.Compose.desktopVersion\n  implementation(\"org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:1.11.0-beta01\")\n  // keep in sync with Dependencies.Kotlin.version\n  implementation(\"org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10\")\n  implementation(\"org.jetbrains.dokka:dokka-gradle-plugin:2.2.0\")\n  implementation(kotlin(\"script-runtime\"))\n}"
  },
  {
    "path": "buildSrc/src/main/kotlin/Dependencies.kt",
    "content": "object BuildPlugins {\n  // keep in sync with buildSrc/build.gradle.kts\n  val androidGradlePlugin = \"com.android.tools.build:gradle:9.1.0\"\n}\n\nobject AndroidX {\n  val appcompat = \"androidx.appcompat:appcompat:1.7.1\"\n}\n\nobject Network {\n  val okHttp = \"com.squareup.okhttp3:okhttp:4.9.0\"\n}\n\nobject Kotlin {\n  // keep in sync with buildSrc/build.gradle.kts\n  val version = \"2.3.10\"\n  val binaryCompatibilityValidatorPlugin = \"org.jetbrains.kotlinx:binary-compatibility-validator:0.9.0\"\n  val gradlePlugin = \"org.jetbrains.kotlin:kotlin-gradle-plugin:$version\"\n\n  val composeCompilerPlugin = \"org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:$version\"\n\n  object Test {\n    val common = \"org.jetbrains.kotlin:kotlin-test-common\"\n    val annotations = \"org.jetbrains.kotlin:kotlin-test-annotations-common\"\n    val jdk = \"org.jetbrains.kotlin:kotlin-test-junit\"\n  }\n}\n\nval ktlint = \"org.jlleitschuh.gradle:ktlint-gradle:10.0.0\"\n\nobject Compose {\n  val desktopVersion = \"1.11.0-beta01\"\n\n  val jetbrainsComposePlugin = \"org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:$desktopVersion\"\n  val activity = \"androidx.activity:activity-compose:1.8.2\"\n  val toolingData = \"androidx.compose.ui:ui-tooling-data:1.6.0\"\n  val coil = \"io.coil-kt.coil3:coil-compose:3.3.0\"\n  val coilHttp = \"io.coil-kt.coil3:coil-network-okhttp:3.3.0\"\n}\n\nobject Commonmark {\n  private val version = \"0.26.0\"\n  val core = \"org.commonmark:commonmark:$version\"\n  val tables = \"org.commonmark:commonmark-ext-gfm-tables:$version\"\n  val strikethrough = \"org.commonmark:commonmark-ext-gfm-strikethrough:$version\"\n  val autolink = \"org.commonmark:commonmark-ext-autolink:$version\"\n}\n\nobject AndroidConfiguration {\n  val minSdk = 23\n  val targetSdk = 36\n  val compileSdk = targetSdk\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/richtext-kmp-library.gradle.kts",
    "content": "import AndroidConfiguration.compileSdk\nimport AndroidConfiguration.minSdk\nimport AndroidConfiguration.targetSdk\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n  kotlin(\"multiplatform\")\n  id(\"com.android.kotlin.multiplatform.library\")\n  id(\"org.jetbrains.kotlin.plugin.compose\")\n  id(\"org.jetbrains.compose\")\n  id(\"com.vanniktech.maven.publish\")\n  id(\"org.jetbrains.dokka\")\n  signing\n}\n\nrepositories {\n  google()\n  mavenCentral()\n}\n\nsigning {\n  val signingKey = System.getenv(\"GPG_PRIVATE_KEY\")?.replace(\"\\\\n\", \"\\n\")\n  val signingPassword = System.getenv(\"GPG_PRIVATE_PASSWORD\")\n  if (signingKey != null && signingPassword != null) {\n    useInMemoryPgpKeys(signingKey, signingPassword)\n  }\n}\n\n// Maven Central credentials are provided via ORG_GRADLE_PROJECT_mavenCentralUsername\n// and ORG_GRADLE_PROJECT_mavenCentralPassword environment variables.\nmavenPublishing {\n  publishToMavenCentral()\n  signAllPublications()\n}\n\nkotlin {\n  jvm()\n  explicitApi()\n\n  android {\n    compileSdk = 36\n\n    compilerOptions {\n      jvmTarget.set(JvmTarget.JVM_11)\n    }\n  }\n}\n\n"
  },
  {
    "path": "desktop-sample/build.gradle.kts",
    "content": "import org.jetbrains.compose.desktop.application.dsl.TargetFormat\n\nplugins {\n  kotlin(\"jvm\")\n  id(\"org.jetbrains.compose\")\n  id(\"org.jetbrains.kotlin.plugin.compose\")\n}\n\ndependencies {\n  implementation(project(\":richtext-commonmark\"))\n  implementation(project(\":richtext-ui-material\"))\n  implementation(compose.desktop.currentOs)\n  implementation(\"org.jetbrains.compose.material:material-icons-extended:1.7.3\")\n}\n\ncompose.desktop {\n  application {\n    mainClass = \"com.halilibo.richtext.desktop.MarkdownSampleAppKt\"\n    nativeDistributions {\n      targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)\n      packageName = \"jvm\"\n      packageVersion = \"1.0.0\"\n    }\n  }\n}"
  },
  {
    "path": "desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/MarkdownSampleApp.kt",
    "content": "package com.halilibo.richtext.desktop\n\nimport androidx.compose.foundation.LocalScrollbarStyle\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.defaultScrollbarStyle\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.foundation.text.selection.DisableSelection\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.Icon\nimport androidx.compose.material.LeadingIconTab\nimport androidx.compose.material.Slider\nimport androidx.compose.material.Surface\nimport androidx.compose.material.TabRow\nimport androidx.compose.material.Text\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.Info\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.singleWindowApplication\nimport com.halilibo.richtext.commonmark.CommonmarkAstNodeParser\nimport com.halilibo.richtext.commonmark.Markdown\nimport com.halilibo.richtext.markdown.BasicMarkdown\nimport com.halilibo.richtext.markdown.node.AstDocument\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.ui.CodeBlockStyle\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.currentRichTextStyle\nimport com.halilibo.richtext.ui.material.RichText\nimport com.halilibo.richtext.ui.resolveDefaults\n\nfun main(): Unit = singleWindowApplication(\n  title = \"RichText KMP\"\n) {\n  var richTextStyle by remember {\n    mutableStateOf(\n      RichTextStyle(\n        codeBlockStyle = CodeBlockStyle(wordWrap = true)\n      ).resolveDefaults()\n    )\n  }\n\n  Surface {\n    CompositionLocalProvider(\n      LocalScrollbarStyle provides defaultScrollbarStyle().copy(\n        hoverColor = Color.DarkGray,\n        unhoverColor = Color.Gray\n      )\n    ) {\n      SelectionContainer {\n        val state = rememberTextFieldState(sampleMarkdown)\n        Row(\n          modifier = Modifier\n            .padding(32.dp)\n            .fillMaxSize(),\n          horizontalArrangement = Arrangement.spacedBy(32.dp)\n        ) {\n          Column(modifier = Modifier.weight(1f)) {\n            DisableSelection {\n              RichTextStyleConfig(richTextStyle = richTextStyle, onChanged = { richTextStyle = it })\n            }\n            BasicTextField(\n              state = state,\n              modifier = Modifier\n                .fillMaxHeight()\n                .background(Color.LightGray)\n                .padding(8.dp)\n            )\n          }\n          var selectedTab by remember { mutableStateOf(0) }\n          Column(Modifier.weight(1f)) {\n            DisableSelection {\n              TabRow(selectedTab) {\n                LeadingIconTab(\n                  selected = selectedTab == 0,\n                  onClick = { selectedTab = 0 },\n                  text = { Text(\"Normal\") },\n                  icon = { Icon(Icons.Default.Info, \"\") })\n                LeadingIconTab(\n                  selected = selectedTab == 1,\n                  onClick = { selectedTab = 1 },\n                  text = { Text(\"Lazy\") },\n                  icon = { Icon(Icons.Default.Favorite, \"\") })\n              }\n            }\n            ProvidePrintUriHandler {\n              if (selectedTab == 0) {\n                RichText(\n                  modifier = Modifier.verticalScroll(rememberScrollState()),\n                  style = richTextStyle,\n                ) {\n                  Markdown(content = state.text.toString())\n                }\n              } else {\n                val parser = remember { CommonmarkAstNodeParser() }\n\n                val astNode = remember(parser) {\n                  parser.parse(sampleMarkdown)\n                }\n\n                RichText(\n                  style = richTextStyle,\n                ) {\n                  LazyMarkdown(astNode)\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * A function that renders Markdown content lazily at the top level. All markdown trees start with\n * an AstDocument. If a document is long enough, usually there are more than hundred child nodes\n * under the root. Then in turn rendering the whole content into the internal column of\n * `BasicRichText` becomes extremely inefficient. Instead, this renderer at least relieves the top\n * level rendering by turning the internal column into a LazyColumn. All other nodes below the\n * first level are rendered as usual.\n *\n * @param astNode Root node of Markdown tree. This can be obtained via a parser.\n */\n@Composable\nfun RichTextScope.LazyMarkdown(astNode: AstNode) {\n  require(astNode.type == AstDocument) {\n    \"Lazy Markdown rendering requires root level node to have a type of AstDocument.\"\n  }\n  // keep the same blockSpacing\n  val currentStyle = currentRichTextStyle\n  val resolvedStyle = remember(currentStyle) { currentStyle.resolveDefaults() }\n  val blockSpacing = with(LocalDensity.current) {\n    resolvedStyle.paragraphSpacing!!.toDp()\n  }\n  LazyColumn(verticalArrangement = Arrangement.spacedBy(blockSpacing)) {\n    var iter = astNode.links.firstChild\n    while (iter != null) {\n      // We need to store iter in a final variable because composition of `item` happens after\n      // iteration\n      val node = iter\n      item {\n        BasicMarkdown(node)\n      }\n      iter = iter.links.next\n    }\n  }\n}\n\n@Composable\nfun RichTextStyleConfig(\n  richTextStyle: RichTextStyle,\n  onChanged: (RichTextStyle) -> Unit\n) {\n  Column(modifier = Modifier.fillMaxWidth()) {\n    Row {\n      Column(Modifier.weight(1f)) {\n        Text(\"Paragraph spacing:\\n${richTextStyle.paragraphSpacing}\")\n        Slider(\n          value = richTextStyle.paragraphSpacing!!.value,\n          valueRange = 0f..20f,\n          onValueChange = {\n            onChanged(richTextStyle.copy(paragraphSpacing = it.sp))\n          }\n        )\n      }\n      Column(Modifier.weight(1f)) {\n        Text(\"List item spacing:\\n${richTextStyle.listStyle!!.itemSpacing}\")\n        Slider(\n          value = richTextStyle.listStyle!!.itemSpacing!!.value,\n          valueRange = 0f..20f,\n          onValueChange = {\n            onChanged(\n              richTextStyle.copy(\n                listStyle = richTextStyle.listStyle!!.copy(\n                  itemSpacing = it.sp\n                )\n              )\n            )\n          }\n        )\n      }\n    }\n    Row {\n      Column(Modifier.weight(1f)) {\n        Text(\"Table cell padding:\\n${richTextStyle.tableStyle!!.cellPadding}\")\n        Slider(\n          value = richTextStyle.tableStyle!!.cellPadding!!.value,\n          valueRange = 0f..20f,\n          onValueChange = {\n            onChanged(\n              richTextStyle.copy(\n                tableStyle = richTextStyle.tableStyle!!.copy(\n                  cellPadding = it.sp\n                )\n              )\n            )\n          }\n        )\n      }\n      Column(Modifier.weight(1f)) {\n        Text(\"Table border width padding:\\n${richTextStyle.tableStyle!!.borderStrokeWidth!!}\")\n        Slider(\n          value = richTextStyle.tableStyle!!.borderStrokeWidth!!,\n          valueRange = 0f..20f,\n          onValueChange = {\n            onChanged(\n              richTextStyle.copy(\n                tableStyle = richTextStyle.tableStyle!!.copy(\n                  borderStrokeWidth = it\n                )\n              )\n            )\n          }\n        )\n      }\n    }\n  }\n}\n\n@Composable\nfun ProvidePrintUriHandler(content: @Composable () -> Unit) {\n  val uriHandler = remember {\n    object : UriHandler {\n      override fun openUri(uri: String) {\n        println(\"Link clicked destination=$uri\")\n      }\n    }\n  }\n\n  CompositionLocalProvider(LocalUriHandler provides uriHandler, content)\n}\n\nprivate val sampleMarkdown = \"\"\"\n  # Demo\n  Based on [this cheatsheet][cheatsheet]\n\n  ---\n\n  ## Headers\n  ---\n  # Header 1\n  ## Header 2\n  ### Header 3\n  #### Header 4\n  ##### Header 5\n  ###### Header 6\n  ---\n  \n  ## Full-bleed Image\n  ![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png)\n\n  ## Images smaller than the width should center\n  ![](https://cdn.nostr.build/p/4a84.png)\n  \n  On LineHeight bug, the image below goes over this text. \n  ![](https://cdn.nostr.build/p/PxZ0.jpg)\n\n  ## Emphasis\n\n  Emphasis, aka italics, with *asterisks* or _underscores_.\n\n  Strong emphasis, aka bold, with **asterisks** or __underscores__.\n\n  Combined emphasis with **asterisks and _underscores_**.\n\n  ---\n\n  ## Lists\n  1. First ordered list item\n  2. Another item\n      * Unordered sub-list.\n  1. Actual numbers don't matter, just that it's a number\n      1. Ordered sub-list\n  4. And another item.\n\n      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).\n\n      To have a line break without a paragraph, you will need to use two trailing spaces.\n      Note that this line is separate, but within the same paragraph.\n      (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)\n\n  * Unordered list can use asterisks\n  - Or minuses\n  + Or pluses\n<!-- -->\n  2. Ordered list starting with `2.`\n  3. Another item\n<!-- -->\n  0. Ordered list starting with `0.`\n<!-- -->\n  003. Ordered list starting with `003.`\n<!-- -->\n  -1. Starting with `-1.` should not be list\n\n\n  ---\n\n  ## Links\n\n  [I'm an inline-style link](https://www.google.com)\n\n  [I'm a reference-style link][Arbitrary case-insensitive reference text]\n\n  [I'm a relative reference to a repository file](../blob/master/LICENSE)\n\n  [You can use numbers for reference-style link definitions][1]\n\n  Or leave it empty and use the [link text itself].\n  \n  Autolink option will detect text links like https://www.google.com and turn them into Markdown links automatically.\n\n  ---\n\n  ## Code\n\n  Inline `code` has `back-ticks around` it.\n\n  ```javascript\n  var s = \"JavaScript syntax highlighting\";\n  alert(s);\n  ```\n\n  ```python\n  s = \"Python syntax highlighting\"\n  print s\n  ```\n\n  ```java\n  /**\n   * Helper method to obtain a Parser with registered strike-through &amp; table extensions\n   * &amp; task lists (added in 1.0.1)\n   *\n   * @return a Parser instance that is supported by this library\n   * @since 1.0.0\n   */\n  @NonNull\n  public static Parser createParser() {\n    return new Parser.Builder()\n        .extensions(Arrays.asList(\n            StrikethroughExtension.create(),\n            TablesExtension.create(),\n            TaskListExtension.create()\n        ))\n        .build();\n  }\n  ```\n\n  ```xml\n  <ScrollView\n    android:id=\"@+id/scroll_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginTop=\"?android:attr/actionBarSize\">\n\n    <TextView\n      android:id=\"@+id/text\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_margin=\"16dip\"\n      android:lineSpacingExtra=\"2dip\"\n      android:textSize=\"16sp\"\n      tools:text=\"yo\\nman\" />\n\n  </ScrollView>\n  ```\n\n  ```\n  No language indicated, so no syntax highlighting.\n  But let's throw in a <b>tag</b>.\n  ```\n  \n  ---\n\n  ## Images\n  \n  Inline-style:\n   \n  ![random image](https://picsum.photos/seed/picsum/400/400)\n  \n  ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  \n  Reference-style:\n   \n  ![random image][logo]\n  \n  [logo]: https://picsum.photos/seed/picsum2/400/400 \"Text 2\"\n  \n  Base64 Inline\n  \n  ![][image1]\n\n  ---\n\n  ## Tables\n\n  Colons can be used to align columns.\n\n  | Tables        | Are           | Cool  |\n  | ------------- |:-------------:| -----:|\n  | col 3 is      | right-aligned | ${'$'}1600 |\n  | col 2 is      | centered      |   ${'$'}12 |\n  | zebra stripes | are neat      |    ${'$'}1 |\n\n  There must be at least 3 dashes separating each header cell.\n  The outer pipes (|) are optional, and you don't need to make the\n  raw Markdown line up prettily. You can also use inline Markdown.\n\n  Markdown | Less | Pretty\n  --- | --- | ---\n  *Still* | `renders` | ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n  1 | 2 | 3\n\n  ---\n\n  ## Blockquotes\n\n  > Blockquotes are very handy in email to emulate reply text.\n  > This line is part of the same quote.\n\n  Quote break.\n\n  > 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.\n\n  Nested quotes\n  > Hello!\n  >> And to you!\n\n  ---\n\n  ## Inline HTML\n\n  ```html\n  <u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>\n  ```\n\n  <body><u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u></body>\n\n  ---\n\n  ## Horizontal Rule\n\n  Three or more...\n\n  ---\n\n  Hyphens (`-`)\n\n  ***\n\n  Asterisks (`*`)\n\n  ___\n\n  Underscores (`_`)\n\n\n  ## License\n\n  ```\n    Copyright 2019 Dimitry Ivanov (legal@noties.io)\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n        http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n  ```\n\n  [cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet\n\n  [arbitrary case-insensitive reference text]: https://www.mozilla.org\n  [1]: http://slashdot.org\n  [link text itself]: http://www.reddit.com\n  \n  [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=>\n\"\"\".trimIndent()"
  },
  {
    "path": "desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/RichTextSampleApp.kt",
    "content": "package com.halilibo.richtext.desktop\n\nimport androidx.compose.animation.Animatable\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.keyframes\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.LocalScrollbarStyle\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.defaultScrollbarStyle\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.Surface\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.em\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.singleWindowApplication\nimport com.halilibo.richtext.ui.BlockQuote\nimport com.halilibo.richtext.ui.CodeBlock\nimport com.halilibo.richtext.ui.CodeBlockStyle\nimport com.halilibo.richtext.ui.FormattedList\nimport com.halilibo.richtext.ui.Heading\nimport com.halilibo.richtext.ui.HorizontalRule\nimport com.halilibo.richtext.ui.InfoPanel\nimport com.halilibo.richtext.ui.InfoPanelType\nimport com.halilibo.richtext.ui.ListType\nimport com.halilibo.richtext.ui.ListType.Ordered\nimport com.halilibo.richtext.ui.ListType.Unordered\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.Table\nimport com.halilibo.richtext.ui.material.RichText\nimport com.halilibo.richtext.ui.resolveDefaults\nimport com.halilibo.richtext.ui.string.InlineContent\nimport com.halilibo.richtext.ui.string.RichTextString.Builder\nimport com.halilibo.richtext.ui.string.RichTextString.Format\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Bold\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Code\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Italic\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Link\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Subscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Superscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Underline\nimport com.halilibo.richtext.ui.string.Text\nimport com.halilibo.richtext.ui.string.richTextString\nimport com.halilibo.richtext.ui.string.withFormat\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\nfun main(): Unit = singleWindowApplication(\n  title = \"RichText KMP\"\n) {\n  var richTextStyle by remember {\n    mutableStateOf(\n      RichTextStyle(\n        codeBlockStyle = CodeBlockStyle(wordWrap = true)\n      ).resolveDefaults()\n    )\n  }\n\n  Surface {\n    CompositionLocalProvider(\n      LocalScrollbarStyle provides defaultScrollbarStyle().copy(\n        hoverColor = Color.DarkGray,\n        unhoverColor = Color.Gray\n      )\n    ) {\n      SelectionContainer {\n        Row(\n          modifier = Modifier\n            .padding(32.dp)\n            .fillMaxSize(),\n          horizontalArrangement = Arrangement.spacedBy(32.dp)\n        ) {\n          Column(modifier = Modifier.weight(1f)) {\n            RichTextStyleConfig(richTextStyle = richTextStyle, onChanged = { richTextStyle = it })\n          }\n          Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {\n            RichTextDemo(style = richTextStyle)\n          }\n        }\n      }\n    }\n  }\n}\n\n@Composable fun RichTextDemo(\n  style: RichTextStyle? = null,\n  header: String = \"\"\n) {\n  RichText(\n    modifier = Modifier.padding(8.dp),\n    style = style\n  ) {\n    Heading(0, \"Paragraphs $header\")\n    Text(\"Simple paragraph.\")\n    Text(\"Paragraph with\\nmultiple lines.\")\n    Text(\"Paragraph with really long line that should be getting wrapped.\")\n    TextPreview()\n\n    Heading(0, \"Lists\")\n    Heading(1, \"Unordered\")\n    ListDemo(listType = Unordered)\n    Heading(1, \"Ordered\")\n    ListDemo(listType = Ordered)\n\n    Heading(0, \"Horizontal Line\")\n    Text(\"Above line\")\n    HorizontalRule()\n    Text(\"Below line\")\n\n    Heading(0, \"Code Block\")\n    CodeBlock(\n      \"\"\"\n        {\n          \"Hello\": \"world!\"\n        }\n      \"\"\".trimIndent()\n    )\n\n    Heading(0, \"Block Quote\")\n    BlockQuote {\n      Text(\"These paragraphs are quoted.\")\n      Text(\"More text.\")\n      BlockQuote {\n        Text(\"Nested block quote.\")\n      }\n    }\n\n    Heading(0, \"Info Panel\")\n    InfoPanel(InfoPanelType.Primary, \"Only text primary info panel\")\n    InfoPanel(InfoPanelType.Success) {\n      Column {\n        Text(\"Successfully sent some data\")\n        HorizontalRule()\n        BlockQuote {\n          Text(\"This is a quote\")\n        }\n      }\n    }\n\n    Heading(0, \"Table\")\n    Table(\n      modifier = Modifier.fillMaxWidth(),\n      headerRow = {\n        cell { Text(\"Column 1\") }\n        cell { Text(\"Column 2\") }\n      }) {\n      row {\n        cell { Text(\"Hello\") }\n        cell {\n          CodeBlock(\"Foo bar\")\n        }\n      }\n      row {\n        cell {\n          BlockQuote {\n            Text(\"Stuff\")\n          }\n        }\n        cell { Text(\"Hello world this is a really long line that is going to wrap hopefully\") }\n      }\n    }\n  }\n}\n\n@Composable private fun RichTextScope.ListDemo(listType: ListType) {\n  FormattedList(listType,\n    @Composable {\n      Text(\"First list item\")\n      FormattedList(listType,\n        @Composable { Text(\"Indented 1\") }\n      )\n    },\n    @Composable {\n      Text(\"\")\n    },\n    @Composable {\n      Text(\"hello\")\n    },\n    @Composable {\n      Text(\"Second list item.\")\n      FormattedList(listType,\n        @Composable { Text(\"Indented 2\") }\n      )\n    }\n  )\n}\n\n@Composable fun TextPreview() {\n  var toggleLink by remember { mutableStateOf(false) }\n  val text = remember(toggleLink) {\n    richTextString {\n      appendPreviewSentence(Bold)\n      appendPreviewSentence(Italic)\n      appendPreviewSentence(Underline)\n      appendPreviewSentence(Strikethrough)\n      appendPreviewSentence(Subscript)\n      appendPreviewSentence(Superscript)\n      appendPreviewSentence(Code)\n      appendPreviewSentence(\n        Link(\"\") { toggleLink = !toggleLink },\n        if (toggleLink) \"clicked link\" else \"link\"\n      )\n      append(\"Here, \")\n      appendInlineContent(content = spinningCross)\n      append(\", is an inline image. \")\n      append(\"And here, \")\n      appendInlineContent(content = slowLoadingImage)\n      append(\", is an inline image that loads after some delay.\")\n      append(\"\\n\\n\")\n\n      append(\"Here \")\n      withFormat(Underline) {\n        append(\"is a \")\n        withFormat(Italic) {\n          append(\"longer sentence \")\n          withFormat(Bold) {\n            append(\"with many \")\n            withFormat(Code) {\n              append(\"different \")\n              withFormat(Strikethrough) {\n                append(\"nested\")\n              }\n              append(\" \")\n            }\n          }\n          append(\"styles.\")\n        }\n      }\n    }\n  }\n  RichText {\n    Text(text)\n  }\n}\n\nprivate val spinningCross = InlineContent {\n  val angle = remember { Animatable(0f) }\n  val color = remember { Animatable(Color.Red) }\n  LaunchedEffect(Unit) {\n    val angleAnim = infiniteRepeatable<Float>(\n      animation = tween(durationMillis = 1000, easing = LinearEasing)\n    )\n    launch { angle.animateTo(360f, angleAnim) }\n\n    val colorAnim = infiniteRepeatable<Color>(\n      animation = keyframes {\n        durationMillis = 2500\n        Color.Blue at 500\n        Color.Cyan at 1000\n        Color.Green at 1500\n        Color.Magenta at 2000\n      }\n    )\n    launch { color.animateTo(Color.Yellow, colorAnim) }\n  }\n\n  Canvas(modifier = Modifier\n    .size(12.sp.toDp(), 12.sp.toDp())\n    .padding(2.dp)) {\n    withTransform({ rotate(angle.value, center) }) {\n      val strokeWidth = 3.dp.toPx()\n      val strokeCap = StrokeCap.Round\n      drawLine(\n        color.value,\n        start = Offset(0f, size.height / 2),\n        end = Offset(size.width, size.height / 2),\n        strokeWidth = strokeWidth,\n        cap = strokeCap\n      )\n      drawLine(\n        color.value,\n        start = Offset(size.width / 2, 0f),\n        end = Offset(size.width / 2, size.height),\n        strokeWidth = strokeWidth,\n        cap = strokeCap\n      )\n    }\n  }\n}\n\nval slowLoadingImage = InlineContent {\n  var loaded by rememberSaveable { mutableStateOf(false) }\n  LaunchedEffect(loaded) {\n    if (!loaded) {\n      delay(3000)\n      loaded = true\n    }\n  }\n\n  if (!loaded) {\n    LoadingSpinner()\n  } else {\n    Box(Modifier.clickable(onClick = { loaded = false })) {\n      val size = remember { Animatable(16f) }\n      LaunchedEffect(Unit) { size.animateTo(100f) }\n      Picture(Modifier.size(size.value.sp.toDp()))\n      Text(\n        \"click to refresh\",\n        modifier = Modifier\n          .padding(3.dp)\n          .align(Alignment.Center),\n        fontSize = 8.sp,\n        style = TextStyle(background = Color.LightGray)\n      )\n    }\n  }\n}\n\n@Composable private fun LoadingSpinner() {\n  val alpha = remember { Animatable(1f) }\n  LaunchedEffect(Unit) {\n    val anim = infiniteRepeatable<Float>(\n      animation = keyframes {\n        durationMillis = 500\n        0f at 250\n        1f at 500\n      })\n    alpha.animateTo(0f, anim)\n  }\n  Text(\n    \"⏳\",\n    fontSize = 3.em,\n    modifier = Modifier\n      .wrapContentSize(Alignment.Center)\n      .graphicsLayer(alpha = alpha.value)\n  )\n}\n\n@Composable private fun Picture(modifier: Modifier) {\n  Canvas(modifier) {\n    drawRect(Color.LightGray)\n    drawLine(Color.Red, Offset(0f, 0f), Offset(size.width, size.height))\n    drawLine(Color.Red, Offset(0f, size.height), Offset(size.width, 0f))\n  }\n}\n\n@OptIn(ExperimentalStdlibApi::class)\nprivate fun Builder.appendPreviewSentence(\n  format: Format,\n  text: String = format.javaClass.simpleName.replaceFirstChar { it.lowercase() }\n) {\n  append(\"Here is some \")\n  withFormat(format) {\n    append(text)\n  }\n  append(\" text. \")\n}\n\n"
  },
  {
    "path": "docs/index.md",
    "content": "# Overview\n\n[![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)\n[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)\n\nCompose Richtext is a collection of Compose libraries for working with rich text formatting and\nMarkdown rendering.  \n\n`richtext-ui`, `richtext-markdown`, `richtext-commonmark`, and `richtext-ui-material`|`richtext-ui-material3` are Kotlin Multiplatform(KMP) Compose Libraries with the exception of iOS.\nAll these modules can be used in Android and Desktop Compose apps. \n\nEach library is documented separately, see the navigation menu for the list. This site also includes\nan API reference.\n\n!!! warning\n    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.\n    There are no tests and some things might be broken or very non-performant.\n\n    The API may also change between releases without deprecation cycles.\n\n## Getting started\n\nThese libraries are published to Maven Central, so just add a Gradle dependency:\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:<LIBRARY-ARTIFACT>:${richtext_version}\")\n}\n```\n\nThere is no difference for KMP artifacts. For instance, if you are adding `richtext-ui` to a Kotlin Multiplatform module\n\n```kotlin\nval commonMain by getting {\n  dependencies {\n    implementation(\"com.halilibo.compose-richtext:richtext-ui:${richtext_version}\")\n  }\n}\n```\n\n### Library Artifacts\n\nThe `LIBRARY_ARTIFACT`s for each individual library can be found on their respective pages.\n\n## Samples\n\nPlease 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)\nprojects to see various use cases of RichText in both platforms."
  },
  {
    "path": "docs/richtext-commonmark.md",
    "content": "# Commonmark Markdown\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)\n[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)\n\nLibrary for parsing and rendering Markdown in Compose using [CommonMark](https://github.com/commonmark/commonmark-java)\nlibrary/spec to parse, and [richtext-markdown](../richtext-markdown/) to render.\n\n## Gradle\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:richtext-commonmark:${richtext_version}\")\n}\n```\n\n## Parsing\n\n`richtext-markdown` module renders a given Markdown Abstract Syntax Tree. It accepts a root \n`AstNode`. This library gives you a parser called `CommonmarkAstNodeParser` to easily convert any \nString to an `AstNode` that represents the Markdown tree.\n\n```kotlin\n    val parser = CommonmarkAstNodeParser()\n    val astNode = parser.parse(\n        \"\"\"\n        # Demo\n        \n        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.\n        \n        1. First ordered list item\n        2. Another item\n            * Unordered sub-list.\n        3. And another item.\n            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).\n        \n        * Unordered list can use asterisks\n        - Or minuses\n        + Or pluses\n        \"\"\".trimIndent()\n    )\n    // ...\n  \n    RichTextScope.BasicMarkdown(astNode)\n```\n\n## Rendering\n\nThe simplest way to render markdown is just pass a string to the [`Markdown`](../api/richtext-commonmark/com.halilibo.richtext.markdown/-markdown.html)\ncomposable under RichText scope:\n\n~~~kotlin\nRichText(\n  modifier = Modifier.padding(16.dp)\n) {\n  Markdown(\n    \"\"\"\n    # Demo\n\n    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.\n\n    1. First ordered list item\n    2. Another item\n        * Unordered sub-list.\n    3. And another item.\n        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).\n\n    * Unordered list can use asterisks\n    - Or minuses\n    + Or pluses\n    ---\n\n    ```javascript\n    var s = \"code blocks use monospace font\";\n    alert(s);\n    ```\n\n    Markdown | Table | Extension\n    --- | --- | ---\n    *renders* | `beautiful images` | ![random image](https://picsum.photos/seed/picsum/400/400 \"Text 1\")\n    1 | 2 | 3\n\n    > Blockquotes are very handy in email to emulate reply text.\n    > This line is part of the same quote.\n    \"\"\".trimIndent()\n  )\n}\n~~~\n\nWhich produces something like this:\n\n![markdown demo](img/markdown-demo.png)\n\n## [`MarkdownParseOptions`](../api/richtext-commonmark/com.halilibo.richtext.commonmark/-markdown-parse-options.html)\n\nPassing `MarkdownParseOptions` into either `Markdown` composable or `CommonmarkAstNodeParser.parse` method provides the ability to control some aspects of the markdown parser:\n\n```kotlin\nval markdownParseOptions = MarkdownParseOptions(\n  autolink = false\n)\n\nMarkdown(\n  markdownParseOptions = markdownParseOptions\n)\n```\n"
  },
  {
    "path": "docs/richtext-markdown.md",
    "content": "# Markdown\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)\n[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)\n\nLibrary for rendering Markdown tree that is defined as an `AstNode`. This module would be useless\nfor someone who is looking to just render a Markdown string. Please check out \n`richtext-commonmark` for such features. `richtext-markdown` behaves as sort of a building block.\nYou can create your own parser or use 3rd party ones that converts any Markdown string to an \n`AstNode` tree.\n\n## Gradle\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:richtext-markdown:${richtext_version}\")\n}\n```\n\n## Rendering\n\nThe simplest way to render markdown is just pass an `AstNode` to the [`BasicMarkdown`](../api/richtext-markdown/com.halilibo.richtext.markdown/-basic-markdown.html)\ncomposable under RichText scope:\n\n~~~kotlin\nRichText(\n  modifier = Modifier.padding(16.dp)\n) {\n  // requires richtext-commonmark module.\n  val parser = remember(options) { CommonmarkAstNodeParser(options) }\n  val astNode = remember(parser) {\n    parser.parse(\n      \"\"\"\n        # Demo\n        \n        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.\n        \n        1. First ordered list item\n        2. Another item\n            * Unordered sub-list.\n        3. And another item.\n            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).\n        \n        * Unordered list can use asterisks\n        - Or minuses\n        + Or pluses\n        \"\"\".trimIndent()\n    )\n  }\n  BasicMarkdown(astNode)\n}\n~~~\n"
  },
  {
    "path": "docs/richtext-ui-material.md",
    "content": "# Richtext UI Material\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)\n[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)\n\nLibrary that makes RichText compatible with Material design in Compose.\n\n## Gradle\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:richtext-ui-material:${richtext_version}\")\n}\n```\n\n## Usage\n\nMaterial RichText library provides a single composable called `RichText` which automatically passes\ndown Material theming attributes to `BasicRichText`. \n\n### [`RichText`](../api/richtext-ui-material/com.halilibo.richtext.ui.material/-rich-text.html)\n\n`RichText` composable wraps around regular `BasicRichText` while introducing the necessary integration\ndependencies. `RichText` shares the exact arguments with regular `BasicRichText`.\n\n```kotlin\nRichText(modifier = Modifier.background(color = Color.White)) {\n  Heading(0, \"Paragraphs\")\n  Text(\"Simple paragraph.\")\n  ...\n}\n```\n"
  },
  {
    "path": "docs/richtext-ui-material3.md",
    "content": "# Richtext UI Material 3\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)\n[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)\n\nLibrary that makes RichText compatible with Material design in Compose.\n\n## Gradle\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:richtext-ui-material3:${richtext_version}\")\n}\n```\n\n## Usage\n\nMaterial3 RichText library provides a single composable called `RichText` which automatically passes\ndown Material3 theming attributes to `BasicRichText`.\n\n### [`RichText`](../api/richtext-ui-material/com.halilibo.richtext.ui.material3/-rich-text.html)\n\n`RichText` composable wraps around regular `BasicRichText` while introducing the necessary integration\ndependencies. `RichText` shares the exact arguments with regular `BasicRichText`.\n\n```kotlin\nRichText(modifier = Modifier.background(color = Color.White)) {\n  Heading(0, \"Paragraphs\")\n  Text(\"Simple paragraph.\")\n  ...\n}\n```\n"
  },
  {
    "path": "docs/richtext-ui.md",
    "content": "# Richtext UI\n\n[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies)\n[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html)\n\nA library of Composables for formatting text using higher-level concepts than are not supported by\ncompose foundation, such as \"ordered lists\" and \"headings\".\n\nRichtext UI is a base library that is non-opinionated about higher level design requirements.\nIf you are already using `MaterialTheme` in your compose app, you can jump to [RichText UI Material](../richtext-ui-material/index.html)\nfor a quick start. There is also Material3 flavor at [RichText UI Material3](../richtext-ui-material3/index.html)\n\n## Gradle\n\n```kotlin\ndependencies {\n  implementation(\"com.halilibo.compose-richtext:richtext-ui:${richtext_version}\")\n}\n```\n\n## [`BasicRichText`](../api/richtext-ui/com.halilibo.richtext.ui/-basic-rich-text.html)\n\nRichtext UI does not depend on Material artifact of Compose. Design agnostic API allows anyone\nto adopt Richtext UI and its extensions like Markdown to their own design and typography systems.\nHence, just like `foundation` and `material` modules of Compose, this library also names the \nbuilding block with `Basic` prefix.\n\nIf you are planning to adopt RichText within your design system, please go ahead and check out [`RichText Material`](../richtext-ui-material/index.html)\nfor inspiration.\n\n## [`RichTextScope`](../api/richtext-ui/com.halilibo.richtext.ui/-rich-text-scope/index.html)\n\n`RichTextScope` is a context wrapper around Composables that integrate and play well within Richtext\ncontent. \n\n## [`RichTextThemeProvider`](../api/richtext-ui/com.halilibo.richtext.ui/-rich-text-theme-provider.html)\n\nEntry point for integrating app's own typography and theme system with BasicRichText.\n\nAPI for this integration is highly influenced by how compose-material theming\nis designed. RichText library assumes that almost all Theme/Design systems would\nhave composition locals that provide a TextStyle downstream.\n\nMoreover, text style should not include text color by best practice. Content color\nexists to figure out text color in the current context. Light/Dark theming leverages content\ncolor to influence not just text but other parts of theming as well.\n\n## Example\n\nOpen the `Demo.kt` file in the `sample` module to play with this. Although the mentioned demo\nuses Material integrated version of `RichText`, they share exactly the same API.\n\n```kotlin\nBasicRichText(\n  modifier = Modifier.background(color = Color.White)\n) {\n  Heading(0, \"Paragraphs\")\n  Text(\"Simple paragraph.\")\n  Text(\"Paragraph with\\nmultiple lines.\")\n  Text(\"Paragraph with really long line that should be getting wrapped.\")\n\n  Heading(0, \"Lists\")\n  Heading(1, \"Unordered\")\n  ListDemo(listType = Unordered)\n  Heading(1, \"Ordered\")\n  ListDemo(listType = Ordered)\n\n  Heading(0, \"Horizontal Line\")\n  Text(\"Above line\")\n  HorizontalRule()\n  Text(\"Below line\")\n\n  Heading(0, \"Code Block\")\n  CodeBlock(\n    \"\"\"\n      {\n        \"Hello\": \"world!\"\n      }\n    \"\"\".trimIndent()\n  )\n\n  Heading(0, \"Block Quote\")\n  BlockQuote {\n    Text(\"These paragraphs are quoted.\")\n    Text(\"More text.\")\n    BlockQuote {\n      Text(\"Nested block quote.\")\n    }\n  }\n\n  Heading(0, \"Info Panel\")\n  InfoPanel(InfoPanelType.Primary, \"Only text primary info panel\")\n  InfoPanel(InfoPanelType.Success) {\n    Column {\n      Text(\"Successfully sent some data\")\n      HorizontalRule()\n      BlockQuote {\n        Text(\"This is a quote\")\n      }\n    }\n  }\n\n  Heading(0, \"Table\")\n  Table(headerRow = {\n    cell { Text(\"Column 1\") }\n    cell { Text(\"Column 2\") }\n  }) {\n    row {\n      cell { Text(\"Hello\") }\n      cell {\n        CodeBlock(\"Foo bar\")\n      }\n    }\n    row {\n      cell {\n        BlockQuote {\n          Text(\"Stuff\")\n        }\n      }\n      cell { Text(\"Hello world this is a really long line that is going to wrap hopefully\") }\n    }\n  }\n}\n```\n\nLooks like this:\n\n![demo rendering](img/richtext-demo.png)\n"
  },
  {
    "path": "gen_dokka_docs.sh",
    "content": "#!/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# Clear out the old API docs\n[ -d docs/api ] && rm -r docs/api\n# Build the docs with dokka\n./gradlew dokkaGenerate --stacktrace\n\n# Create a copy of our docs at our $DOCS_ROOT\ncp -a docs/* $DOCS_ROOT\n\n# Convert docs/xxx.md links to just xxx/\nsed -i.bak 's/docs\\/\\([a-zA-Z-]*\\).md/\\1/' $DOCS_ROOT/index.md\n\n# Finally delete all of the backup files\nfind . -name '*.bak' -delete"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app\"s APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n\n# Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308)\nsystemProp.org.gradle.internal.publish.checksums.insecure=true\n\nGROUP=com.halilibo.compose-richtext\nVERSION_NAME=1.0.0-alpha04\n\nPOM_DESCRIPTION=A collection of Compose libraries for advanced text formatting and alternative display types.\n\nPOM_INCEPTION_YEAR=2020\nPOM_URL=https://github.com/halilozercan/compose-richtext\nPOM_SCM_URL=https://github.com/halilozercan/compose-richtext/\nPOM_SCM_CONNECTION=scm:git:git://github.com/halilozercan/compose-richtext.git\nPOM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/halilozercan/compose-richtext.git\nPOM_LICENSE_NAME=The Apache Software License, Version 2.0\nPOM_LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt\nPOM_LICENSE_DIST=repo\nPOM_DEVELOPER_ID=halilozercan\nPOM_DEVELOPER_NAME=Halil Ozercan\n\norg.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers\n\nkotlin.mpp.stability.nowarn=true\nkotlin.mpp.androidSourceSetLayoutVersion=2\nandroid.defaults.buildfeatures.resvalues=true\nandroid.sdk.defaultTargetSdkToCompileSdkIfUnset=false\nandroid.enableAppCompileTimeRClass=false\nandroid.usesSdkInManifest.disallowed=false\nandroid.uniquePackageNames=false\nandroid.dependency.useConstraints=true\nandroid.r8.strictFullModeForKeepRules=false\nandroid.r8.optimizedResourceShrinking=false\nandroid.builtInKotlin=false\nandroid.newDsl=false"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd \"${APP_HOME:-./}\" > /dev/null && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@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 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Compose Richtext\nrepo_name: compose-richtext\nrepo_url: https://github.com/halilozercan/compose-richtext\nsite_description: \"A collection of Compose libraries for advanced text formatting and alternative display types.\"\nsite_author: Halil Ozercan\nsite_url: https://halilibo.com/compose-richtext\nremote_branch: gh-pages\nedit_uri: edit/main/docs/\n\ndocs_dir: docs-gen\n\ntheme:\n  name: material\n  icon:\n    repo: fontawesome/brands/github\n  features:\n    - navigation.instant\n    - toc.autohide\n\nmarkdown_extensions:\n  - toc:\n      permalink: true\n  - pymdownx.superfences\n  - pymdownx.tabbed\n  - admonition\n\nnav:\n  - index.md\n  - richtext-ui-material.md\n  - richtext-ui-material3.md\n  - richtext-ui.md\n  - richtext-markdown.md\n  - richtext-commonmark.md\n  - 'API Reference': api/\n  - Changelog: https://github.com/halilozercan/compose-richtext/releases\n"
  },
  {
    "path": "richtext-commonmark/build.gradle.kts",
    "content": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n\n  android {\n    namespace = \"com.halilibo.richtext.commonmark\"\n  }\n  sourceSets {\n    val commonMain by getting {\n      dependencies {\n        implementation(compose.runtime)\n        api(project(\":richtext-ui\"))\n        api(project(\":richtext-markdown\"))\n      }\n    }\n    val commonTest by getting\n\n    val jvmAndroidMain by creating {\n      dependsOn(commonMain)\n      dependencies {\n        implementation(Commonmark.core)\n        implementation(Commonmark.tables)\n        implementation(Commonmark.strikethrough)\n        implementation(Commonmark.autolink)\n      }\n    }\n\n    val jvmAndroidTest by creating {\n      dependsOn(commonTest)\n      dependencies {\n        implementation(Kotlin.Test.jdk)\n      }\n    }\n\n    val androidMain by getting {\n      dependsOn(jvmAndroidMain)\n    }\n\n    val jvmMain by getting {\n      dependsOn(jvmAndroidMain)\n    }\n\n    val jvmTest by getting {\n      dependsOn(jvmAndroidTest)\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-commonmark/gradle.properties",
    "content": "POM_NAME=Compose Richtext Commonmark\nPOM_DESCRIPTION=A library for rendering markdown in Compose using the Commonmark library."
  },
  {
    "path": "richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/CommonMarkdownParseOptions.kt",
    "content": "package com.halilibo.richtext.commonmark\n\n/**\n * Allows configuration of the Markdown parser\n *\n * @param autolink Detect plain text links and turn them into Markdown links.\n */\npublic class CommonMarkdownParseOptions(\n  public val autolink: Boolean\n) {\n\n  override fun toString(): String {\n    return \"CommonMarkdownParseOptions(autolink=$autolink)\"\n  }\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is CommonMarkdownParseOptions) return false\n\n    return autolink == other.autolink\n  }\n\n  override fun hashCode(): Int {\n    return autolink.hashCode()\n  }\n\n  public fun copy(\n    autolink: Boolean = this.autolink\n  ): CommonMarkdownParseOptions = CommonMarkdownParseOptions(\n    autolink = autolink\n  )\n\n  public companion object {\n    public val Default: CommonMarkdownParseOptions = CommonMarkdownParseOptions(\n      autolink = true\n    )\n  }\n}\n"
  },
  {
    "path": "richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt",
    "content": "package com.halilibo.richtext.commonmark\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport com.halilibo.richtext.markdown.AstBlockNodeComposer\nimport com.halilibo.richtext.markdown.BasicMarkdown\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.ui.RichTextScope\n\n/**\n * A composable that renders Markdown content according to Commonmark specification using RichText.\n *\n * @param content Markdown text. No restriction on length.\n * @param markdownParseOptions Options for the Markdown parser.\n * @param astBlockNodeComposer An interceptor to take control of composing any block type node's\n * rendering. Use it to render images, html text, tables with your own components.\n */\n@Composable\npublic fun RichTextScope.Markdown(\n  content: String,\n  markdownParseOptions: CommonMarkdownParseOptions = CommonMarkdownParseOptions.Default,\n  astBlockNodeComposer: AstBlockNodeComposer? = null\n) {\n  val commonmarkAstNodeParser = remember(markdownParseOptions) {\n    CommonmarkAstNodeParser(markdownParseOptions)\n  }\n\n  val astRootNode by produceState<AstNode?>(\n    initialValue = null,\n    key1 = commonmarkAstNodeParser,\n    key2 = content\n  ) {\n    value = commonmarkAstNodeParser.parse(content)\n  }\n\n  astRootNode?.let {\n    BasicMarkdown(astNode = it, astBlockNodeComposer = astBlockNodeComposer)\n  }\n}\n\n/**\n * A helper class that can convert any text content into an ASTNode tree and return its root.\n */\npublic expect class CommonmarkAstNodeParser(\n  options: CommonMarkdownParseOptions = CommonMarkdownParseOptions.Default\n) {\n\n  /**\n   * Parse markdown content and return Abstract Syntax Tree(AST).\n   *\n   * @param text Markdown text to be parsed.\n   * @param options Options for the Commonmark Markdown parser.\n   */\n  public fun parse(text: String): AstNode\n}"
  },
  {
    "path": "richtext-commonmark/src/jvmAndroidMain/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt",
    "content": "package com.halilibo.richtext.commonmark\n\nimport com.halilibo.richtext.markdown.node.AstBlockQuote\nimport com.halilibo.richtext.markdown.node.AstCode\nimport com.halilibo.richtext.markdown.node.AstDocument\nimport com.halilibo.richtext.markdown.node.AstEmphasis\nimport com.halilibo.richtext.markdown.node.AstFencedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstHardLineBreak\nimport com.halilibo.richtext.markdown.node.AstHeading\nimport com.halilibo.richtext.markdown.node.AstHtmlBlock\nimport com.halilibo.richtext.markdown.node.AstHtmlInline\nimport com.halilibo.richtext.markdown.node.AstImage\nimport com.halilibo.richtext.markdown.node.AstIndentedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstLink\nimport com.halilibo.richtext.markdown.node.AstLinkReferenceDefinition\nimport com.halilibo.richtext.markdown.node.AstListItem\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstNodeLinks\nimport com.halilibo.richtext.markdown.node.AstNodeType\nimport com.halilibo.richtext.markdown.node.AstOrderedList\nimport com.halilibo.richtext.markdown.node.AstParagraph\nimport com.halilibo.richtext.markdown.node.AstSoftLineBreak\nimport com.halilibo.richtext.markdown.node.AstStrikethrough\nimport com.halilibo.richtext.markdown.node.AstStrongEmphasis\nimport com.halilibo.richtext.markdown.node.AstTableBody\nimport com.halilibo.richtext.markdown.node.AstTableCell\nimport com.halilibo.richtext.markdown.node.AstTableCellAlignment\nimport com.halilibo.richtext.markdown.node.AstTableHeader\nimport com.halilibo.richtext.markdown.node.AstTableRoot\nimport com.halilibo.richtext.markdown.node.AstTableRow\nimport com.halilibo.richtext.markdown.node.AstText\nimport com.halilibo.richtext.markdown.node.AstThematicBreak\nimport com.halilibo.richtext.markdown.node.AstUnorderedList\nimport org.commonmark.ext.autolink.AutolinkExtension\nimport org.commonmark.ext.gfm.strikethrough.Strikethrough\nimport org.commonmark.ext.gfm.strikethrough.StrikethroughExtension\nimport org.commonmark.ext.gfm.tables.TableBlock\nimport org.commonmark.ext.gfm.tables.TableBody\nimport org.commonmark.ext.gfm.tables.TableCell\nimport org.commonmark.ext.gfm.tables.TableCell.Alignment.CENTER\nimport org.commonmark.ext.gfm.tables.TableCell.Alignment.LEFT\nimport org.commonmark.ext.gfm.tables.TableCell.Alignment.RIGHT\nimport org.commonmark.ext.gfm.tables.TableHead\nimport org.commonmark.ext.gfm.tables.TableRow\nimport org.commonmark.ext.gfm.tables.TablesExtension\nimport org.commonmark.node.BlockQuote\nimport org.commonmark.node.BulletList\nimport org.commonmark.node.Code\nimport org.commonmark.node.CustomBlock\nimport org.commonmark.node.CustomNode\nimport org.commonmark.node.Document\nimport org.commonmark.node.Emphasis\nimport org.commonmark.node.FencedCodeBlock\nimport org.commonmark.node.HardLineBreak\nimport org.commonmark.node.Heading\nimport org.commonmark.node.HtmlBlock\nimport org.commonmark.node.HtmlInline\nimport org.commonmark.node.Image\nimport org.commonmark.node.IndentedCodeBlock\nimport org.commonmark.node.Link\nimport org.commonmark.node.LinkReferenceDefinition\nimport org.commonmark.node.ListItem\nimport org.commonmark.node.Node\nimport org.commonmark.node.OrderedList\nimport org.commonmark.node.Paragraph\nimport org.commonmark.node.SoftLineBreak\nimport org.commonmark.node.StrongEmphasis\nimport org.commonmark.node.Text\nimport org.commonmark.node.ThematicBreak\nimport org.commonmark.parser.Parser\n\n/**\n * Holds the data for a pending conversion task in the iterative tree traversal.\n */\nprivate class ConvertWorkItem(\n  val startNode: Node,\n  val parentAstNode: AstNode?,\n  val initialPrev: AstNode?,\n  val onFirstCreated: (AstNode?) -> Unit\n)\n\n/**\n * Maps a CommonMark [Node] to its corresponding [AstNodeType].\n * Returns null for unrecognized node types (CustomNode, CustomBlock, etc.).\n */\nprivate fun convertNodeType(node: Node): AstNodeType? = when (node) {\n  is BlockQuote -> AstBlockQuote\n  is BulletList -> AstUnorderedList(bulletMarker = node.bulletMarker)\n  is Code -> AstCode(literal = node.literal)\n  is Document -> AstDocument\n  is Emphasis -> AstEmphasis(delimiter = node.openingDelimiter)\n  is FencedCodeBlock -> AstFencedCodeBlock(\n    literal = node.literal,\n    fenceChar = node.fenceChar,\n    fenceIndent = node.fenceIndent,\n    fenceLength = node.fenceLength,\n    info = node.info\n  )\n  is HardLineBreak -> AstHardLineBreak\n  is Heading -> AstHeading(\n    level = node.level\n  )\n  is ThematicBreak -> AstThematicBreak\n  is HtmlInline -> AstHtmlInline(\n    literal = node.literal\n  )\n  is HtmlBlock -> AstHtmlBlock(\n    literal = node.literal\n  )\n  is Image -> {\n    if (node.destination == null) {\n      null\n    }\n    else {\n      AstImage(\n        title = node.title ?: \"\",\n        destination = node.destination\n      )\n    }\n  }\n  is IndentedCodeBlock -> AstIndentedCodeBlock(\n    literal = node.literal\n  )\n  is Link -> AstLink(\n    title = node.title ?: \"\",\n    destination = node.destination\n  )\n  is ListItem -> AstListItem\n  is OrderedList -> AstOrderedList(\n    startNumber = node.startNumber,\n    delimiter = node.delimiter\n  )\n  is Paragraph -> AstParagraph\n  is SoftLineBreak -> AstSoftLineBreak\n  is StrongEmphasis -> AstStrongEmphasis(\n    delimiter = node.openingDelimiter\n  )\n  is Text -> AstText(\n    literal = node.literal\n  )\n  is LinkReferenceDefinition -> AstLinkReferenceDefinition(\n    title = node.title ?: \"\",\n    destination = node.destination,\n    label = node.label\n  )\n  is TableBlock -> AstTableRoot\n  is TableHead -> AstTableHeader\n  is TableBody -> AstTableBody\n  is TableRow -> AstTableRow\n  is TableCell -> AstTableCell(\n    header = node.isHeader,\n    alignment = when (node.alignment) {\n      LEFT -> AstTableCellAlignment.LEFT\n      CENTER -> AstTableCellAlignment.CENTER\n      RIGHT -> AstTableCellAlignment.RIGHT\n      null -> AstTableCellAlignment.LEFT\n      else -> AstTableCellAlignment.LEFT\n    }\n  )\n  is Strikethrough -> AstStrikethrough(\n    node.openingDelimiter\n  )\n  is CustomNode -> null\n  is CustomBlock -> null\n  else -> null\n}\n\n/**\n * Converts common-markdown tree to AstNode tree iteratively using an explicit stack,\n * avoiding StackOverflowError on deeply nested or long markdown documents.\n */\ninternal fun convert(\n  node: Node?,\n  parentNode: AstNode? = null,\n  previousNode: AstNode? = null,\n): AstNode? {\n  node ?: return null\n\n  var result: AstNode? = null\n  val stack = ArrayDeque<ConvertWorkItem>()\n  stack.addLast(ConvertWorkItem(node, parentNode, previousNode) { result = it })\n\n  while (stack.isNotEmpty()) {\n    val item = stack.removeLast()\n\n    var prev: AstNode? = null\n    var firstCreated: AstNode? = null\n    var cmNode: Node? = item.startNode\n    var nullTypeNode: Node? = null\n\n    // Iterate through siblings instead of recursing\n    while (cmNode != null) {\n      val nodeType = convertNodeType(cmNode)\n      val newNode = nodeType?.let {\n        AstNode(it, AstNodeLinks(\n          parent = item.parentAstNode,\n          previous = prev ?: item.initialPrev\n        ))\n      }\n\n      if (newNode != null) {\n        if (firstCreated == null) firstCreated = newNode\n        prev?.links?.next = newNode\n\n        // Push child processing onto the explicit stack instead of recursing\n        val child = cmNode.firstChild\n        if (child != null) {\n          stack.addLast(ConvertWorkItem(child, newNode, null) { newNode.links.firstChild = it })\n        }\n\n        prev = newNode\n        cmNode = cmNode.next\n      } else {\n        // Unrecognized node type — stop sibling chain (preserves original behavior)\n        nullTypeNode = cmNode\n        cmNode = null\n      }\n    }\n\n    // Set lastChild on the parent, matching the original recursive behavior\n    if (nullTypeNode != null) {\n      if (nullTypeNode.next == null) {\n        item.parentAstNode?.links?.lastChild = null\n      }\n    } else {\n      item.parentAstNode?.links?.lastChild = prev\n    }\n\n    item.onFirstCreated(firstCreated)\n  }\n\n  return result\n}\n\npublic actual class CommonmarkAstNodeParser actual constructor(\n  options: CommonMarkdownParseOptions\n) {\n\n  private val parser = Parser.builder()\n    .extensions(\n      listOfNotNull(\n        TablesExtension.create(),\n        StrikethroughExtension.create(),\n        if (options.autolink) AutolinkExtension.create() else null\n      )\n    )\n    .build()\n\n  public actual fun parse(text: String): AstNode {\n    val commonmarkNode = parser.parse(text)\n      ?: throw IllegalArgumentException(\n        \"Could not parse the given text content into a meaningful Markdown representation!\"\n      )\n\n    return convert(commonmarkNode)\n      ?: throw IllegalArgumentException(\n        \"Could not convert the generated Commonmark Node into an ASTNode!\"\n      )\n  }\n}\n\n"
  },
  {
    "path": "richtext-commonmark/src/jvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport com.halilibo.richtext.commonmark.CommonMarkdownParseOptions\nimport com.halilibo.richtext.commonmark.CommonmarkAstNodeParser\nimport com.halilibo.richtext.commonmark.convert\nimport com.halilibo.richtext.markdown.node.AstBlockQuote\nimport com.halilibo.richtext.markdown.node.AstDocument\nimport com.halilibo.richtext.markdown.node.AstHeading\nimport com.halilibo.richtext.markdown.node.AstImage\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstNodeLinks\nimport com.halilibo.richtext.markdown.node.AstParagraph\nimport com.halilibo.richtext.markdown.node.AstText\nimport org.commonmark.node.Document\nimport org.commonmark.node.Image\nimport org.commonmark.node.Paragraph\nimport org.commonmark.node.Text\nimport org.junit.Test\nimport java.util.concurrent.atomic.AtomicReference\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertNull\nimport kotlin.test.assertSame\nimport kotlin.test.assertTrue\nimport kotlin.test.fail\n\ninternal class AstNodeConvertKtTest {\n\n  private val parser = CommonmarkAstNodeParser(CommonMarkdownParseOptions.Default)\n\n  @Test\n  fun `when image without title is converted, then the content description is empty`() {\n    val destination = \"/url\"\n    val image = Image(destination, null)\n\n    val result = convert(image)\n\n    assertEquals(\n      expected = AstNode(\n        type = AstImage(title = \"\", destination = destination),\n        links = AstNodeLinks()\n      ),\n      actual = result\n    )\n  }\n\n  @Test\n  fun `tree links are correctly wired for a document with siblings`() {\n    val root = parser.parse(\"# Heading\\n\\nParagraph text\")\n\n    // Root should be a Document\n    assertEquals(AstDocument, root.type)\n    assertNull(root.links.parent)\n\n    // First child should be the heading\n    val heading = root.links.firstChild\n    assertNotNull(heading)\n    assertEquals(AstHeading(level = 1), heading.type)\n    assertSame(root, heading.links.parent)\n    assertNull(heading.links.previous)\n\n    // Second child should be the paragraph\n    val paragraph = heading.links.next\n    assertNotNull(paragraph)\n    assertEquals(AstParagraph, paragraph.type)\n    assertSame(root, paragraph.links.parent)\n    assertSame(heading, paragraph.links.previous)\n    assertNull(paragraph.links.next)\n\n    // lastChild should point to the paragraph\n    assertSame(paragraph, root.links.lastChild)\n  }\n\n  @Test\n  fun `tree links are correctly wired for nested structures`() {\n    val root = parser.parse(\"> quoted text\")\n\n    val blockquote = root.links.firstChild\n    assertNotNull(blockquote)\n    assertEquals(AstBlockQuote, blockquote.type)\n    assertSame(root, blockquote.links.parent)\n\n    val paragraph = blockquote.links.firstChild\n    assertNotNull(paragraph)\n    assertEquals(AstParagraph, paragraph.type)\n    assertSame(blockquote, paragraph.links.parent)\n\n    val text = paragraph.links.firstChild\n    assertNotNull(text)\n    assertEquals(AstText(literal = \"quoted text\"), text.type)\n    assertSame(paragraph, text.links.parent)\n  }\n\n  @Test\n  fun `document with many sibling paragraphs does not overflow`() {\n    // 2000 paragraphs would cause ~2000 frames of sibling recursion\n    val markdown = (1..2000).joinToString(\"\\n\\n\") { \"Paragraph $it\" }\n    val root = parser.parse(markdown)\n\n    assertEquals(AstDocument, root.type)\n\n    // Walk the sibling chain and count paragraphs\n    var count = 0\n    var node = root.links.firstChild\n    while (node != null) {\n      assertEquals(AstParagraph, node.type)\n      count++\n      node = node.links.next\n    }\n    assertEquals(2000, count)\n\n    // lastChild should be the final paragraph\n    assertNotNull(root.links.lastChild)\n    assertNull(root.links.lastChild!!.links.next)\n  }\n\n  @Test\n  fun `deeply nested blockquotes do not overflow`() {\n    // 500 levels of nested blockquotes would cause ~500 frames of child recursion\n    val markdown = \">\".repeat(500) + \" deep text\"\n    val root = parser.parse(markdown)\n\n    assertEquals(AstDocument, root.type)\n\n    // Walk down the child chain counting blockquotes\n    var depth = 0\n    var node = root.links.firstChild\n    while (node != null && node.type is AstBlockQuote) {\n      depth++\n      node = node.links.firstChild\n    }\n    assertEquals(500, depth)\n\n    // The innermost blockquote should contain a paragraph with text\n    assertNotNull(node)\n    assertEquals(AstParagraph, node.type)\n  }\n\n  /**\n   * Proves that the sibling chain depth we test would overflow a recursive implementation\n   * on a constrained thread stack (similar to Android's ~1MB default), while our iterative\n   * convert() handles it without issue.\n   */\n  @Test\n  fun `convert handles long sibling chains that would overflow a recursive implementation`() {\n    val siblingCount = 5000\n    // Build a CommonMark tree directly: Document -> Paragraph(\"1\") -> Paragraph(\"2\") -> ...\n    val doc = Document()\n    for (i in 1..siblingCount) {\n      val para = Paragraph()\n      para.appendChild(Text(\"$i\"))\n      doc.appendChild(para)\n    }\n\n    val stackSize = 256L * 1024 // 256KB — smaller than Android's default ~1MB\n\n    // First, prove this stack size is too small for equivalent-depth recursion.\n    // A simple recursive chain of siblingCount depth will overflow.\n    val recursionOverflowed = AtomicReference<Boolean>(false)\n    val recursionThread = Thread(null, {\n      try {\n        countRecursively(siblingCount)\n      } catch (_: StackOverflowError) {\n        recursionOverflowed.set(true)\n      }\n    }, \"recursion-test\", stackSize)\n    recursionThread.start()\n    recursionThread.join()\n    assertTrue(\n      recursionOverflowed.get(),\n      \"Expected StackOverflowError for $siblingCount recursive calls on ${stackSize / 1024}KB stack\"\n    )\n\n    // Now prove our iterative convert() handles the same depth on the same stack size.\n    val convertError = AtomicReference<Throwable?>(null)\n    val convertResult = AtomicReference<AstNode?>(null)\n    val convertThread = Thread(null, {\n      try {\n        convertResult.set(convert(doc))\n      } catch (e: Throwable) {\n        convertError.set(e)\n      }\n    }, \"convert-test\", stackSize)\n    convertThread.start()\n    convertThread.join()\n\n    val error = convertError.get()\n    if (error != null) {\n      fail(\"convert() should not throw on a long sibling chain, but threw: $error\")\n    }\n\n    val root = convertResult.get()\n    assertNotNull(root, \"convert() should return a non-null root\")\n    assertEquals(AstDocument, root.type)\n\n    // Verify the full sibling chain was converted\n    var count = 0\n    var node = root.links.firstChild\n    while (node != null) {\n      count++\n      node = node.links.next\n    }\n    assertEquals(siblingCount, count)\n  }\n}\n\n/** Simple recursive function that recurses [n] times to demonstrate stack overflow. */\nprivate fun countRecursively(n: Int): Int {\n  if (n <= 0) return 0\n  return 1 + countRecursively(n - 1)\n}\n"
  },
  {
    "path": "richtext-markdown/build.gradle.kts",
    "content": "plugins {\n  id(\"richtext-kmp-library\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo.richtext.markdown\"\n  }\n  sourceSets {\n    val commonMain by getting {\n      dependencies {\n        implementation(compose.runtime)\n        implementation(compose.foundation)\n        api(project(\":richtext-ui\"))\n      }\n    }\n    val commonTest by getting\n\n    val androidMain by getting {\n      dependencies {\n        implementation(Compose.coil)\n        implementation(Compose.coilHttp)\n      }\n    }\n\n    val jvmMain by getting {\n      dependencies {\n        implementation(compose.desktop.currentOs)\n        implementation(Network.okHttp)\n      }\n    }\n\n    val jvmTest by getting {\n      dependencies {\n        implementation(Kotlin.Test.jdk)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-markdown/gradle.properties",
    "content": "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",
    "content": "<manifest />"
  },
  {
    "path": "richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.fromHtml\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.string.Text\nimport com.halilibo.richtext.ui.string.richTextString\n\n@Composable\ninternal actual fun RichTextScope.HtmlBlock(content: String) {\n  val richTextString = remember(content) {\n    richTextString {\n      withAnnotatedString {\n        append(AnnotatedString.Companion.fromHtml(content))\n      }\n    }\n  }\n  Text(richTextString)\n}\n"
  },
  {
    "path": "richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/MarkdownImage.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport android.annotation.SuppressLint\nimport android.util.Base64\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.BoxWithConstraintsScope\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport coil3.compose.rememberAsyncImagePainter\nimport coil3.request.ImageRequest\nimport coil3.request.crossfade\nimport coil3.size.Size\n\nprivate val DEFAULT_IMAGE_SIZE = 64.dp\n\n/**\n * Implementation of MarkdownImage by using Coil library for Android.\n */\n@Composable\ninternal actual fun MarkdownImage(\n  url: String,\n  contentDescription: String?,\n  modifier: Modifier,\n  contentScale: ContentScale\n) {\n  val data = if (url.startsWith(\"data:image\") && url.contains(\"base64\")) {\n    val base64ImageString = url.substringAfter(\"base64,\")\n    Base64.decode(base64ImageString, Base64.DEFAULT)\n  } else {\n    url\n  }\n  val painter = rememberAsyncImagePainter(\n    ImageRequest.Builder(LocalContext.current)\n      .data(data = data)\n      .size(Size.ORIGINAL)\n      .crossfade(true)\n      .build()\n  )\n\n  @SuppressLint(\"UnusedBoxWithConstraintsScope\")\n  BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {\n    val painterState by painter.state.collectAsState()\n    val sizeModifier = renderInSize(painterState.painter?.intrinsicSize)\n\n    Image(\n      painter = painter,\n      contentDescription = contentDescription,\n      modifier = sizeModifier,\n      contentScale = contentScale\n    )\n  }\n}\n\n@Composable\npublic fun BoxWithConstraintsScope.renderInSize(\n  painterIntrinsicSize: androidx.compose.ui.geometry.Size?,\n): Modifier {\n  val density = LocalDensity.current\n\n  val sizeModifier = if (painterIntrinsicSize != null &&\n    painterIntrinsicSize.isSpecified &&\n    painterIntrinsicSize.width != Float.POSITIVE_INFINITY &&\n    painterIntrinsicSize.height != Float.POSITIVE_INFINITY\n  ) {\n    val width = painterIntrinsicSize.width\n    val height = painterIntrinsicSize.height\n    val scale = if (width > constraints.maxWidth) {\n      constraints.maxWidth.toFloat() / width\n    } else {\n      1f\n    }\n\n    with(density) {\n      Modifier.size(\n        (width * scale).toDp(),\n        (height * scale).toDp()\n      )\n    }\n  } else {\n    // if size is not defined at all, Coil fails to render the image\n    // here, we give a default size for images until they are loaded.\n    Modifier.size(DEFAULT_IMAGE_SIZE)\n  }\n\n  return sizeModifier\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/BasicMarkdown.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.heading\nimport androidx.compose.ui.semantics.semantics\nimport com.halilibo.richtext.markdown.node.AstBlockNodeType\nimport com.halilibo.richtext.markdown.node.AstBlockQuote\nimport com.halilibo.richtext.markdown.node.AstDocument\nimport com.halilibo.richtext.markdown.node.AstFencedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstHeading\nimport com.halilibo.richtext.markdown.node.AstHtmlBlock\nimport com.halilibo.richtext.markdown.node.AstIndentedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstInlineNodeType\nimport com.halilibo.richtext.markdown.node.AstLinkReferenceDefinition\nimport com.halilibo.richtext.markdown.node.AstListItem\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstOrderedList\nimport com.halilibo.richtext.markdown.node.AstParagraph\nimport com.halilibo.richtext.markdown.node.AstTableBody\nimport com.halilibo.richtext.markdown.node.AstTableCell\nimport com.halilibo.richtext.markdown.node.AstTableHeader\nimport com.halilibo.richtext.markdown.node.AstTableRoot\nimport com.halilibo.richtext.markdown.node.AstTableRow\nimport com.halilibo.richtext.markdown.node.AstText\nimport com.halilibo.richtext.markdown.node.AstThematicBreak\nimport com.halilibo.richtext.markdown.node.AstUnorderedList\nimport com.halilibo.richtext.ui.BlockQuote\nimport com.halilibo.richtext.ui.CodeBlock\nimport com.halilibo.richtext.ui.FormattedList\nimport com.halilibo.richtext.ui.Heading\nimport com.halilibo.richtext.ui.HorizontalRule\nimport com.halilibo.richtext.ui.ListType.Ordered\nimport com.halilibo.richtext.ui.ListType.Unordered\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.string.InlineContent\nimport com.halilibo.richtext.ui.string.Text\nimport com.halilibo.richtext.ui.string.richTextString\n\n/**\n * A composable that renders Markdown content pointed by [astNode] into this [RichTextScope].\n * Designed to be a building block that should be wrapped with a specific parser.\n *\n * @param astNode Root node of Markdown tree. This can be obtained via a parser.\n * @param astBlockNodeComposer An interceptor to take control of composing any block type node's\n * rendering. Use it to render images, html text, tables with your own components.\n */\n@Composable\npublic fun RichTextScope.BasicMarkdown(\n  astNode: AstNode,\n  astBlockNodeComposer: AstBlockNodeComposer? = null\n) {\n  RecursiveRenderMarkdownAst(astNode, astBlockNodeComposer)\n}\n\n/**\n * An interface used to intercept block type AstNode rendering logic to inject custom composables\n * for nodes that satisfy [predicate].\n */\npublic interface AstBlockNodeComposer {\n\n  /**\n   * Returns true if [Compose] function would handle this [astBlockNodeType].\n   */\n  public fun predicate(astBlockNodeType: AstBlockNodeType): Boolean\n\n  /**\n   * A composable that's responsible for composing the given [astNode] if its [AstNode.type]\n   * returned true from [predicate]. This composable should also decide when and where to render\n   * its children, then call [visitChildren] with a reference to which node's children to visit.\n   * This is not an enforced behavior but unknowingly failing to do so can cause loss of\n   * information during rendering.\n   */\n  @Composable\n  public fun RichTextScope.Compose(\n    astNode: AstNode,\n    visitChildren: @Composable (AstNode) -> Unit\n  )\n}\n\n/**\n * When parsed, markdown content or any other rich text can be represented as a tree.\n * The default markdown parser that is used in this project `common-markdown` also\n * utilizes the said approach. Although there are ways to iteratively traverse a tree,\n * it is more readable and compose-friendly to do it recursively.\n *\n * This function basically receives a node from the tree, root or any node, and then\n * recursively travels along the nodes while spitting out or wrapping composables around\n * the content. RichText API is highly compatible with this method.\n *\n * However, there are multiple assumptions to increase predictability. Despite the fact\n * that every [AstNode] can have another [AstNode] as a child, it should not be that\n * generic in Markdown content. For example, a Text node must not have any other children.\n * That's why this function does not have 100% coverage for all [AstNode] types.\n *\n * Heading, Paragraph are considered to be main text containers. Their content is regarded\n * as one block and children traversal happens separately.\n *\n * FormattedList, OrderedList are also content blocks. Their children are filtered before\n * being traversed. Only ListItems are accepted as valid children for these blocks.\n *\n * For now, only tables are rendered from CustomBlock or CustomNode.\n *\n * @param astNode Root node to start rendering.\n */\n@Composable\ninternal fun RichTextScope.RecursiveRenderMarkdownAst(\n  astNode: AstNode?,\n  astNodeComposer: AstBlockNodeComposer?\n) {\n  astNode ?: return\n\n  if (astNodeComposer != null &&\n    astNode.type is AstBlockNodeType &&\n    astNodeComposer.predicate(astNode.type)\n  ) {\n    with(astNodeComposer) {\n      Compose(astNode) {\n        renderChildren(it, astNodeComposer)\n      }\n    }\n  } else {\n    with(DefaultAstNodeComposer) {\n      Compose(\n        astNode = astNode,\n        visitChildren = {\n          renderChildren(it, astNodeComposer)\n        }\n      )\n    }\n  }\n}\n\nprivate val DefaultAstNodeComposer = object : AstBlockNodeComposer {\n  override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean = true\n\n  @Composable\n  override fun RichTextScope.Compose(\n    astNode: AstNode,\n    visitChildren: @Composable (AstNode) -> Unit\n  ) {\n    when (val astNodeType = astNode.type) {\n      is AstDocument -> visitChildren(astNode)\n      is AstBlockQuote -> {\n        BlockQuote {\n          visitChildren(astNode)\n        }\n      }\n\n      is AstUnorderedList -> {\n        FormattedList(\n          listType = Unordered,\n          items = astNode.filterChildrenType<AstListItem>().toList()\n        ) { astListItem ->\n          // if this list item has no child, it should at least emit a single pixel layout.\n          if (astListItem.links.firstChild == null) {\n            BasicText(\"\")\n          } else {\n            visitChildren(astListItem)\n          }\n        }\n      }\n\n      is AstOrderedList -> {\n        FormattedList(\n          listType = Ordered,\n          items = astNode.childrenSequence().toList(),\n          startIndex = astNodeType.startNumber - 1,\n        ) { astListItem ->\n          // if this list item has no child, it should at least emit a single pixel layout.\n          if (astListItem.links.firstChild == null) {\n            BasicText(\"\")\n          } else {\n            visitChildren(astListItem)\n          }\n        }\n      }\n\n      is AstThematicBreak -> {\n        HorizontalRule()\n      }\n\n      is AstHeading -> {\n        Heading(level = astNodeType.level) {\n          MarkdownRichText(astNode, Modifier.semantics { heading() })\n        }\n      }\n\n      is AstIndentedCodeBlock -> {\n        CodeBlock(text = astNodeType.literal.trim())\n      }\n\n      is AstFencedCodeBlock -> {\n        CodeBlock(text = astNodeType.literal.trim())\n      }\n\n      is AstHtmlBlock -> {\n        Text(text = richTextString {\n          appendInlineContent(content = InlineContent {\n            HtmlBlock(astNodeType.literal)\n          })\n        })\n      }\n\n      is AstLinkReferenceDefinition -> {\n        // TODO(halilozercan)\n        /* no-op */\n      }\n\n      is AstParagraph -> {\n        MarkdownRichText(astNode)\n      }\n\n      is AstTableRoot -> {\n        RenderTable(astNode)\n      }\n      // This should almost never happen. All the possible text\n      // nodes must be under either Heading, Paragraph or CustomNode\n      // In any case, we should include it here to prevent any\n      // non-rendered text problems.\n      is AstText -> {\n        // TODO(halilozercan) use multiplatform compatible stderr logging\n        println(\"Unexpected raw text while traversing the Abstract Syntax Tree.\")\n        Text(richTextString { append(astNodeType.literal) })\n      }\n\n      is AstListItem -> {\n        println(\"MarkdownRichText: Unexpected AstListItem while traversing the Abstract Syntax Tree.\")\n      }\n\n      is AstInlineNodeType -> {\n        // ignore\n        println(\"MarkdownRichText: Unexpected AstInlineNodeType $astNodeType while traversing the Abstract Syntax Tree.\")\n      }\n\n      AstTableBody,\n      AstTableHeader,\n      AstTableRow,\n      is AstTableCell -> {\n        println(\"MarkdownRichText: Unexpected Table node while traversing the Abstract Syntax Tree.\")\n      }\n    }.let {}\n  }\n}\n\n/**\n * Visit and render children from first to last.\n *\n * @param node Root ASTNode whose children will be visited.\n */\n@Composable\ninternal fun RichTextScope.renderChildren(\n  node: AstNode?,\n  astNodeComposer: AstBlockNodeComposer?\n) {\n  node?.childrenSequence()?.forEach {\n    RecursiveRenderMarkdownAst(astNode = it, astNodeComposer = astNodeComposer)\n  }\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport com.halilibo.richtext.ui.RichTextScope\n\n/**\n * Android and JVM can have different WebView or HTML rendering implementations.\n * We are leaving HTML rendering to platform side.\n */\n@Composable\ninternal expect fun RichTextScope.HtmlBlock(content: String)\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownImage.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\n\n//TODO(halilozercan): This should be provided from consumer side.\n/**\n * Image rendering is highly platform dependent. Coil is the desired\n * way to show images but it doesn't exist in desktop.\n */\n@Composable\ninternal expect fun MarkdownImage(\n  url: String,\n  contentDescription: String?,\n  modifier: Modifier = Modifier,\n  contentScale: ContentScale\n)\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport com.halilibo.richtext.markdown.node.AstBlockQuote\nimport com.halilibo.richtext.markdown.node.AstCode\nimport com.halilibo.richtext.markdown.node.AstEmphasis\nimport com.halilibo.richtext.markdown.node.AstFencedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstHardLineBreak\nimport com.halilibo.richtext.markdown.node.AstHeading\nimport com.halilibo.richtext.markdown.node.AstImage\nimport com.halilibo.richtext.markdown.node.AstIndentedCodeBlock\nimport com.halilibo.richtext.markdown.node.AstLink\nimport com.halilibo.richtext.markdown.node.AstLinkReferenceDefinition\nimport com.halilibo.richtext.markdown.node.AstListItem\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstParagraph\nimport com.halilibo.richtext.markdown.node.AstSoftLineBreak\nimport com.halilibo.richtext.markdown.node.AstStrikethrough\nimport com.halilibo.richtext.markdown.node.AstStrongEmphasis\nimport com.halilibo.richtext.markdown.node.AstText\nimport com.halilibo.richtext.ui.BlockQuote\nimport com.halilibo.richtext.ui.FormattedList\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.string.InlineContent\nimport com.halilibo.richtext.ui.string.RichTextString\nimport com.halilibo.richtext.ui.string.Text\nimport com.halilibo.richtext.ui.string.withFormat\n\n/**\n * Only render the text content that exists below [astNode]. All the content blocks\n * like [AstBlockQuote] or [AstFencedCodeBlock] are ignored. This composable is\n * suited for [AstHeading] and [AstParagraph] since they are strictly text blocks.\n *\n * Some notes about commonmark and in general Markdown parsing.\n *\n * - Paragraph and Heading are the only RichTextString containers in base implementation.\n *   - RichTextString is build by traversing the children of Heading or Paragraph.\n *   - RichTextString can include;\n *     - Emphasis\n *     - StrongEmphasis\n *     - Image\n *     - Link\n *     - Code\n * - Code blocks should not have any children. Their whole content must reside in\n * [AstIndentedCodeBlock.literal] or [AstFencedCodeBlock.literal].\n * - Blocks like [BlockQuote], [FormattedList], [AstListItem] must have an [AstParagraph]\n * as a child to include any further RichText.\n * - CustomNode and CustomBlock can have their own scope, no idea about that.\n *\n * @param astNode Root node to accept as Text Content container.\n */\n@Composable\ninternal fun RichTextScope.MarkdownRichText(astNode: AstNode, modifier: Modifier = Modifier) {\n  // Assume that only RichText nodes reside below this level.\n  val richText = remember(astNode) {\n    computeRichTextString(astNode)\n  }\n\n  Text(text = richText, modifier = modifier)\n}\n\nprivate fun computeRichTextString(astNode: AstNode): RichTextString {\n  val richTextStringBuilder = RichTextString.Builder()\n\n  // Modified pre-order traversal with pushFormat, popFormat support.\n  var iteratorStack = listOf(\n    AstNodeTraversalEntry(\n      astNode = astNode,\n      isVisited = false,\n      formatIndex = null\n    )\n  )\n\n  while (iteratorStack.isNotEmpty()) {\n    val (currentNode, isVisited, formatIndex) = iteratorStack.first().copy()\n    iteratorStack = iteratorStack.drop(1)\n\n    if (!isVisited) {\n      val newFormatIndex = when (val currentNodeType = currentNode.type) {\n        is AstCode -> {\n          richTextStringBuilder.withFormat(RichTextString.Format.Code) {\n            append(currentNodeType.literal)\n          }\n          null\n        }\n        is AstEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Italic)\n        is AstStrikethrough -> richTextStringBuilder.pushFormat(\n          RichTextString.Format.Strikethrough\n        )\n        is AstImage -> {\n          richTextStringBuilder.appendInlineContent(\n            content = InlineContent(\n              initialSize = {\n                IntSize(128.dp.roundToPx(), 128.dp.roundToPx())\n              }\n            ) {\n              MarkdownImage(\n                url = currentNodeType.destination,\n                contentDescription = currentNodeType.title,\n                modifier = Modifier.fillMaxWidth(),\n                contentScale = ContentScale.Inside\n              )\n            }\n          )\n          null\n        }\n        is AstLink -> richTextStringBuilder.pushFormat(RichTextString.Format.Link(\n          destination = currentNodeType.destination\n        ))\n        is AstSoftLineBreak -> {\n          richTextStringBuilder.append(\" \")\n          null\n        }\n        is AstHardLineBreak -> {\n          richTextStringBuilder.append(\"\\n\")\n          null\n        }\n        is AstStrongEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Bold)\n        is AstText -> {\n          richTextStringBuilder.append(currentNodeType.literal)\n          null\n        }\n        is AstLinkReferenceDefinition -> richTextStringBuilder.pushFormat(\n          RichTextString.Format.Link(destination = currentNodeType.destination))\n        else -> null\n      }\n\n      iteratorStack = iteratorStack.addFirst(\n        AstNodeTraversalEntry(\n          astNode = currentNode,\n          isVisited = true,\n          formatIndex = newFormatIndex\n        )\n      )\n\n      // Do not visit children of terminals such as Text, Image, etc.\n      if (!currentNode.isRichTextTerminal()) {\n        currentNode.childrenSequence(reverse = true).forEach {\n          iteratorStack = iteratorStack.addFirst(\n            AstNodeTraversalEntry(\n              astNode = it,\n              isVisited = false,\n              formatIndex = null\n            )\n          )\n        }\n      }\n    }\n\n    if (formatIndex != null) {\n      richTextStringBuilder.pop(formatIndex)\n    }\n  }\n\n  return richTextStringBuilder.toRichTextString()\n}\n\nprivate data class AstNodeTraversalEntry(\n  val astNode: AstNode,\n  val isVisited: Boolean,\n  val formatIndex: Int?\n)\n\nprivate inline fun <reified T> List<T>.addFirst(item: T): List<T> {\n  return listOf(item) + this\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.runtime.Composable\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstTableBody\nimport com.halilibo.richtext.markdown.node.AstTableCell\nimport com.halilibo.richtext.markdown.node.AstTableHeader\nimport com.halilibo.richtext.markdown.node.AstTableRow\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.Table\n\n@Composable\ninternal fun RichTextScope.RenderTable(node: AstNode) {\n  Table(\n    headerRow = {\n      node.filterChildrenType<AstTableHeader>()\n        .firstOrNull()\n        ?.filterChildrenType<AstTableRow>()\n        ?.firstOrNull()\n        ?.filterChildrenType<AstTableCell>()\n        ?.forEach { tableCell ->\n          cell {\n            MarkdownRichText(tableCell)\n          }\n        }\n    }\n  ) {\n    node.filterChildrenType<AstTableBody>()\n      .firstOrNull()\n      ?.filterChildrenType<AstTableRow>()\n      ?.forEach { tableRow ->\n        row {\n          tableRow.filterChildrenType<AstTableCell>()\n            .forEach { tableCell ->\n              cell {\n                MarkdownRichText(tableCell)\n              }\n            }\n        }\n      }\n  }\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport com.halilibo.richtext.markdown.node.AstCode\nimport com.halilibo.richtext.markdown.node.AstHardLineBreak\nimport com.halilibo.richtext.markdown.node.AstImage\nimport com.halilibo.richtext.markdown.node.AstNode\nimport com.halilibo.richtext.markdown.node.AstNodeType\nimport com.halilibo.richtext.markdown.node.AstSoftLineBreak\nimport com.halilibo.richtext.markdown.node.AstText\n\ninternal fun AstNode.childrenSequence(\n  reverse: Boolean = false\n): Sequence<AstNode> {\n  return if (!reverse) {\n    generateSequence(this.links.firstChild) { it.links.next }\n  } else {\n    generateSequence(this.links.lastChild) { it.links.previous }\n  }\n}\n\n/**\n * Markdown rendering is susceptible to have assumptions. Hence, some rendering rules\n * may force restrictions on children. So, valid children nodes should be selected\n * before traversing. This function returns a LinkedList of children which conforms to\n * [filter] function.\n *\n * @param filter A lambda to select valid children.\n */\ninternal fun AstNode.filterChildren(\n  reverse: Boolean = false,\n  filter: (AstNode) -> Boolean\n): Sequence<AstNode> {\n  return childrenSequence(reverse).filter(filter)\n}\n\ninternal inline fun <reified T : AstNodeType> AstNode.filterChildrenType(): Sequence<AstNode> {\n  return filterChildren { it.type is T }\n}\n\n/**\n * These ASTNode types should never have any children. If any exists, ignore them.\n */\ninternal fun AstNode.isRichTextTerminal(): Boolean {\n  return type is AstText\n          || type is AstCode\n          || type is AstImage\n          || type is AstSoftLineBreak\n          || type is AstHardLineBreak\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt",
    "content": "package com.halilibo.richtext.markdown.node\n\n/**\n * Generic AstNode implementation that can define any node in Abstract Syntax Tree.\n *\n * @param type A sealed class which is categorized into block and inline nodes.\n * @param links Pointers to parent, sibling, child nodes.\n */\npublic class AstNode(\n  public val type: AstNodeType,\n  public val links: AstNodeLinks\n) {\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is AstNode) return false\n\n    if (type != other.type) return false\n    if (links != other.links) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = type.hashCode()\n    result = 31 * result + links.hashCode()\n    return result\n  }\n}\n"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt",
    "content": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\n\n/**\n * All the pointers that can exist for a node in an AST.\n *\n * Links are mutable to make it possible to instantiate a Node which can then reconfigure its\n * children and siblings. Please do not modify the links after an ASTNode is created and the scope\n * is finished.\n */\n@Immutable\npublic class AstNodeLinks(\n  public var parent: AstNode? = null,\n  public var firstChild: AstNode? = null,\n  public var lastChild: AstNode? = null,\n  public var previous: AstNode? = null,\n  public var next: AstNode? = null\n) {\n\n  override fun equals(other: Any?): Boolean {\n    if (other !is AstNodeLinks) return false\n\n    return parent === other.parent &&\n        firstChild === other.firstChild &&\n        lastChild === other.lastChild &&\n        previous === other.previous &&\n        next === other.next\n  }\n\n  /**\n   * Stop infinite loop and only calculate towards bottom-right direction\n   */\n  override fun hashCode(): Int {\n    return (firstChild ?: 0).hashCode() * 11 + (next ?: 0).hashCode() * 7\n  }\n}"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt",
    "content": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\nimport com.halilibo.richtext.ui.string.RichTextString\n\n/**\n * Refer to https://spec.commonmark.org/0.30/#precedence\n *\n * Commonmark specification defines 3 different types of AST nodes;\n *\n * - Container Block\n * - Leaf Block\n * - Inline Content\n *\n * Container blocks are the most generic nodes. They define a structure for their children but\n * do not impose any major restrictions, meaning that container blocks can contain any\n * type of child node.\n *\n * Leaf blocks are self-explanatory, they should not have any children. All the necessary content\n * to render a leaf block should already exist in its payload\n *\n * Inline Content is analogous to [RichTextString] and its styles. Most of the inline content\n * nodes are about styling(bold, italic, strikethrough, code). The rest contains links, images,\n * html content, and of course raw text.\n */\npublic sealed class AstNodeType\n\n//region AstBlockNodeType\n\npublic sealed class AstBlockNodeType: AstNodeType()\n\n//region AstContainerBlockNodeType\n\n/**\n * Defines a subtype of Block Node that can contain other nodes.\n */\npublic sealed class AstContainerBlockNodeType: AstBlockNodeType()\n\n/**\n * Usually defines the root of a markdown document.\n */\n@Immutable\npublic object AstDocument : AstContainerBlockNodeType()\n\n/**\n * A block quote container that will indent its contents relative to its own indentation.\n */\n@Immutable\npublic object AstBlockQuote : AstContainerBlockNodeType()\n\n/**\n * Ordered or Unordered list item.\n */\n@Immutable\npublic object AstListItem : AstContainerBlockNodeType()\n\n/**\n * A list type that marks its items with bullets to signify a lack of order.\n */\n@Immutable\npublic data class AstUnorderedList(\n  val bulletMarker: Char\n) : AstContainerBlockNodeType()\n\n/**\n * A list type that uses numbers to mark its items.\n */\n@Immutable\npublic data class AstOrderedList(\n  val startNumber: Int,\n  val delimiter: Char\n) : AstContainerBlockNodeType()\n\n//endregion\n\n//region AstLeafBlockNodeType\n\n/**\n * Defines a subtype of Block Node that can only contain plain text and full-length annotations.\n */\npublic sealed class AstLeafBlockNodeType: AstBlockNodeType()\n\n@Immutable\npublic object AstThematicBreak : AstLeafBlockNodeType()\n\n@Immutable\npublic data class AstHeading(\n  val level: Int\n) : AstLeafBlockNodeType()\n\n@Immutable\npublic data class AstIndentedCodeBlock(\n  val literal: String\n) : AstLeafBlockNodeType()\n\n@Immutable\npublic data class AstFencedCodeBlock(\n  val fenceChar: Char,\n  val fenceLength: Int,\n  val fenceIndent: Int,\n  val info: String,\n  val literal: String\n) : AstLeafBlockNodeType()\n\n@Immutable\npublic data class AstHtmlBlock(\n  val literal: String\n) : AstLeafBlockNodeType()\n\n@Immutable\npublic data class AstLinkReferenceDefinition(\n  val label: String,\n  val destination: String,\n  val title: String\n) : AstLeafBlockNodeType()\n\n@Immutable\npublic object AstParagraph : AstLeafBlockNodeType()\n\n//endregion\n\n//endregion\n\n//region AstInlineNodeType\n\n/**\n * Defines a node type that can only apply to inline content.\n */\npublic sealed class AstInlineNodeType: AstNodeType()\n\n@Immutable\npublic data class AstCode(\n  val literal: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstEmphasis(\n  private val delimiter: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstStrongEmphasis(\n  private val delimiter: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstStrikethrough(\n  val delimiter: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstLink(\n  val destination: String,\n  val title: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstImage(\n  val title: String,\n  val destination: String\n) : AstInlineNodeType()\n\n@Immutable\npublic data class AstHtmlInline(\n  val literal: String\n) : AstInlineNodeType()\n\n@Immutable\npublic object AstHardLineBreak : AstInlineNodeType()\n\n@Immutable\npublic object AstSoftLineBreak : AstInlineNodeType()\n\n@Immutable\npublic data class AstText(\n  val literal: String\n) : AstInlineNodeType()\n\n//endregion"
  },
  {
    "path": "richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt",
    "content": "package com.halilibo.richtext.markdown.node\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\npublic object AstTableRoot: AstContainerBlockNodeType()\n\n@Immutable\npublic object AstTableBody: AstContainerBlockNodeType()\n\n@Immutable\npublic object AstTableHeader: AstContainerBlockNodeType()\n\n@Immutable\npublic object AstTableRow: AstContainerBlockNodeType()\n\n@Immutable\npublic data class AstTableCell(\n  val header: Boolean,\n  val alignment: AstTableCellAlignment\n) : AstContainerBlockNodeType()\n\npublic enum class AstTableCellAlignment {\n  LEFT,\n  CENTER,\n  RIGHT\n}\n"
  },
  {
    "path": "richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport com.halilibo.richtext.ui.RichTextScope\n\n@Composable\ninternal actual fun RichTextScope.HtmlBlock(content: String) {\n  DisposableEffect(Unit) {\n    println(\"Html blocks are rendered literally in Compose Desktop!\")\n    onDispose {  }\n  }\n  BasicText(content)\n}\n"
  },
  {
    "path": "richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt",
    "content": "package com.halilibo.richtext.markdown\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.produceState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.jetbrains.skia.Image.Companion.makeFromEncoded\nimport java.awt.image.BufferedImage\nimport java.io.ByteArrayOutputStream\nimport java.io.InputStream\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.Base64\nimport javax.imageio.ImageIO\n\n@Composable\ninternal actual fun MarkdownImage(\n  url: String,\n  contentDescription: String?,\n  modifier: Modifier,\n  contentScale: ContentScale\n) {\n  val image by produceState<ImageBitmap?>(null, url) {\n    if (url.startsWith(\"data:image\") && url.contains(\"base64\")) {\n      val base64ImageString = url.substringAfter(\"base64,\")\n      value = makeFromEncoded(Base64.getDecoder().decode(base64ImageString)).toComposeImageBitmap()\n    } else {\n      loadFullImage(url)?.let {\n        value = makeFromEncoded(toByteArray(it)).toComposeImageBitmap()\n      }\n    }\n  }\n\n  if (image != null) {\n    Image(\n      bitmap = image!!,\n      contentDescription = contentDescription,\n      modifier = modifier,\n      contentScale = contentScale\n    )\n  }\n}\n\nprivate fun toByteArray(bitmap: BufferedImage): ByteArray {\n  val baos = ByteArrayOutputStream()\n  ImageIO.write(bitmap, \"png\", baos)\n  return baos.toByteArray()\n}\n\nprivate suspend fun loadFullImage(source: String): BufferedImage? = withContext(Dispatchers.IO) {\n  runCatching {\n    val url = URL(source)\n    val connection: HttpURLConnection = url.openConnection() as HttpURLConnection\n    connection.connectTimeout = 5000\n    connection.connect()\n\n    val input: InputStream = connection.inputStream\n    val bitmap: BufferedImage? = ImageIO.read(input)\n    bitmap\n  }.getOrNull()\n}\n"
  },
  {
    "path": "richtext-ui/build.gradle.kts",
    "content": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo.richtext.ui\"\n  }\n  sourceSets {\n    val commonMain by getting {\n      dependencies {\n        implementation(compose.runtime)\n        implementation(compose.foundation)\n      }\n    }\n    val commonTest by getting\n\n    val jvmAndroidMain by creating {\n      dependsOn(commonMain)\n    }\n\n    val androidMain by getting {\n      dependsOn(jvmAndroidMain)\n    }\n    val jvmMain by getting {\n      dependsOn(jvmAndroidMain)\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/gradle.properties",
    "content": "POM_NAME=Compose Richtext UI\nPOM_DESCRIPTION=A library for rendering high-level text formatting in Compose."
  },
  {
    "path": "richtext-ui/src/androidMain/AndroidManifest.xml",
    "content": "<manifest />"
  },
  {
    "path": "richtext-ui/src/androidMain/kotlin/com/halilibo/richtext/ui/CodeBlock.android.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal actual fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n) {\n  if (!wordWrap) {\n    val scrollState = rememberScrollState()\n    children(Modifier.horizontalScroll(scrollState))\n  } else {\n    children(Modifier)\n  }\n}"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BasicRichText.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Arrangement.spacedBy\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\n\n/**\n * Draws some rich text. Entry point to the compose-richtext library.\n */\n@Composable\npublic fun BasicRichText(\n  modifier: Modifier = Modifier,\n  style: RichTextStyle? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  with(RichTextScope) {\n    RestartListLevel {\n      WithStyle(style) {\n        val resolvedStyle = currentRichTextStyle.resolveDefaults()\n        val blockSpacing = with(LocalDensity.current) {\n          resolvedStyle.paragraphSpacing!!.toDp()\n        }\n\n        Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) {\n          children()\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BlockQuote.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.offset\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.BlockQuoteGutter.BarGutter\n\ninternal val DefaultBlockQuoteGutter = BarGutter()\n\n/**\n * A composable function that draws the gutter beside a [BlockQuote].\n *\n * [BarGutter] is provided as the reasonable default of a simple vertical line.\n */\npublic interface BlockQuoteGutter {\n  @Composable public fun RichTextScope.drawGutter()\n\n  @Immutable\n  public data class BarGutter(\n    val startMargin: TextUnit = 6.sp,\n    val barWidth: TextUnit = 3.sp,\n    val endMargin: TextUnit = 6.sp,\n    val color: (contentColor: Color) -> Color = { it.copy(alpha = .25f) }\n  ) : BlockQuoteGutter {\n\n    @Composable\n    override fun RichTextScope.drawGutter() {\n      with(LocalDensity.current) {\n        val color = color(currentContentColor)\n\n        val modifier = remember(startMargin, endMargin, barWidth, color) {\n          // Padding must come before width.\n          Modifier\n            .padding(\n              start = startMargin.toDp(),\n              end = endMargin.toDp()\n            )\n            .width(barWidth.toDp())\n            .background(color, RoundedCornerShape(50))\n        }\n\n        Box(modifier)\n      }\n    }\n  }\n}\n\n/**\n * Draws a block quote, with a [BlockQuoteGutter] drawn beside the children on the start side.\n */\n@Composable public fun RichTextScope.BlockQuote(children: @Composable RichTextScope.() -> Unit) {\n  val gutter = currentRichTextStyle.resolveDefaults().blockQuoteGutter!!\n  val spacing = with(LocalDensity.current) {\n    currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp() / 2\n  }\n\n  Layout(content = {\n    with(gutter) { drawGutter() }\n    BasicRichText(\n      modifier = Modifier.padding(top = spacing, bottom = spacing),\n      children = children\n    )\n  }) { measurables, constraints ->\n    val gutterMeasurable = measurables[0]\n    val contentsMeasurable = measurables[1]\n\n    // First get the width of the gutter, so we can measure the contents with\n    // the smaller width if bounded.\n    val gutterWidth = gutterMeasurable.minIntrinsicWidth(constraints.maxHeight)\n\n    // Measure the contents with the confined width.\n    // This must be done before measuring the gutter so that the gutter gets\n    // the correct height.\n    val contentsConstraints = constraints.offset(horizontal = -gutterWidth)\n    val contentsPlaceable = contentsMeasurable.measure(contentsConstraints)\n    val layoutWidth = contentsPlaceable.width + gutterWidth\n    val layoutHeight = contentsPlaceable.height\n\n    // Measure the gutter to fit in its min intrinsic width and exactly the\n    // height of the contents.\n    val gutterConstraints = constraints.copy(\n      maxWidth = gutterWidth,\n      minHeight = layoutHeight,\n      maxHeight = layoutHeight\n    )\n    val gutterPlaceable = gutterMeasurable.measure(gutterConstraints)\n\n    layout(layoutWidth, layoutHeight) {\n      gutterPlaceable.place(IntOffset.Zero)\n      contentsPlaceable.place(gutterWidth, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/CodeBlock.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\n\n/**\n * Defines how [CodeBlock]s are rendered.\n *\n * @param textStyle The [TextStyle] to use for the block.\n * @param modifier The [Modifier] to use for the block.\n * @param padding The amount of space between the edge of the text and the edge of the background.\n * @param wordWrap Whether a code block breaks the lines or scrolls horizontally.\n */\n@Immutable\npublic data class CodeBlockStyle(\n  val textStyle: TextStyle? = null,\n  val modifier: Modifier? = null,\n  val padding: TextUnit? = null,\n  val wordWrap: Boolean? = null\n) {\n  public companion object {\n    public val Default: CodeBlockStyle = CodeBlockStyle()\n  }\n}\n\nprivate val DefaultCodeBlockTextStyle = TextStyle(\n  fontFamily = FontFamily.Monospace\n)\ninternal val DefaultCodeBlockBackgroundColor: Color = Color.LightGray.copy(alpha = .5f)\nprivate val DefaultCodeBlockModifier: Modifier =\n  Modifier.background(color = DefaultCodeBlockBackgroundColor)\nprivate val DefaultCodeBlockPadding: TextUnit = 16.sp\nprivate const val DefaultCodeWordWrap: Boolean = true\n\ninternal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle(\n  textStyle = textStyle ?: DefaultCodeBlockTextStyle,\n  modifier = modifier ?: DefaultCodeBlockModifier,\n  padding = padding ?: DefaultCodeBlockPadding,\n  wordWrap = wordWrap ?: DefaultCodeWordWrap\n)\n\n/**\n * A specially-formatted block of text that typically uses a monospace font with a tinted\n * background.\n *\n * @param wordWrap Overrides word wrap preference coming from [CodeBlockStyle]\n */\n@Composable public fun RichTextScope.CodeBlock(\n  text: String,\n  wordWrap: Boolean? = null\n) {\n  CodeBlock(wordWrap = wordWrap) {\n    Text(text)\n  }\n}\n\n/**\n * A specially-formatted block of text that typically uses a monospace font with a tinted\n * background.\n *\n * @param wordWrap Overrides word wrap preference coming from [CodeBlockStyle]\n */\n@Composable public fun RichTextScope.CodeBlock(\n  wordWrap: Boolean? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  val codeBlockStyle = currentRichTextStyle.resolveDefaults().codeBlockStyle!!\n  val textStyle = currentTextStyle.merge(codeBlockStyle.textStyle)\n  val modifier = codeBlockStyle.modifier!!\n  val blockPadding = with(LocalDensity.current) {\n    codeBlockStyle.padding!!.toDp()\n  }\n  val resolvedWordWrap = wordWrap ?: codeBlockStyle.wordWrap!!\n\n  CodeBlockLayout(\n    wordWrap = resolvedWordWrap\n  ) { layoutModifier ->\n    Box(\n      modifier = layoutModifier\n        .then(modifier)\n        .padding(blockPadding)\n    ) {\n      textStyleBackProvider(textStyle) {\n        children()\n      }\n    }\n  }\n}\n\n/**\n * Desktop composable adds an optional horizontal scrollbar.\n */\n@Composable\ninternal expect fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n)\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/FormattedList.kt",
    "content": "@file:Suppress(\"ComposableNaming\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.selection.DisableSelection\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.paint\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.ListType.Ordered\nimport com.halilibo.richtext.ui.ListType.Unordered\nimport kotlin.math.max\n\npublic enum class ListType {\n  /**\n   * An ordered (numbered) list.\n   */\n  Ordered,\n\n  /**\n   * An unordered (bullet) list.\n   */\n  Unordered\n}\n\n/**\n * Defines how to draw list markers for [FormattedList]s that are [Ordered].\n *\n * These are typically some sort of ordinal text.\n */\npublic interface OrderedMarkers {\n  @Composable public fun drawMarker(\n    level: Int,\n    index: Int\n  )\n\n  public companion object {\n    /**\n     * Creates an [OrderedMarkers] from an arbitrary composable given the indentation level and\n     * the index.\n     */\n    public operator fun invoke(\n      drawMarker: @Composable (level: Int, index: Int) -> Unit\n    ): OrderedMarkers = object : OrderedMarkers {\n      @Composable override fun drawMarker(\n        level: Int,\n        index: Int\n      ) {\n        drawMarker(level, index)\n      }\n    }\n  }\n}\n\n/**\n * Creates an [OrderedMarkers] that will cycle through the values in [markers] for each\n * indentation level given the index.\n */\npublic fun RichTextScope.textOrderedMarkers(\n  vararg markers: (index: Int) -> String\n): OrderedMarkers =\n  OrderedMarkers { level, index ->\n    Text(markers[level % markers.size](index))\n  }\n\n/**\n * Defines how to draw list markers for [FormattedList]s that are [Unordered].\n *\n * These are typically some sort of bullet point.\n */\npublic interface UnorderedMarkers {\n  @Composable public fun drawMarker(level: Int)\n\n  public companion object {\n    /**\n     * Creates an [UnorderedMarkers] from an arbitrary composable given the indentation level.\n     */\n    public operator fun invoke(drawMarker: @Composable (level: Int) -> Unit): UnorderedMarkers =\n      object : UnorderedMarkers {\n        @Composable override fun drawMarker(level: Int) = drawMarker(level)\n      }\n  }\n}\n\n/**\n * Creates an [UnorderedMarkers] that will cycle through the values in [markers] for each\n * indentation level.\n */\npublic fun RichTextScope.textUnorderedMarkers(\n  vararg markers: String\n): UnorderedMarkers = UnorderedMarkers {\n  Text(markers[it % markers.size])\n}\n\n/**\n * Creates an [UnorderedMarkers] that will cycle through the values in [painters] for each\n * indentation level.\n */\npublic fun painterUnorderedMarkers(vararg painters: Painter): UnorderedMarkers = UnorderedMarkers {\n  Box(Modifier.paint(painters[it % painters.size]))\n}\n\n/**\n * Defines how [FormattedList]s should look.\n *\n * @param markerIndent The padding before each marker.\n * @param contentsIndent The padding after each marker.\n */\n@Immutable\npublic data class ListStyle(\n  val markerIndent: TextUnit? = null,\n  val contentsIndent: TextUnit? = null,\n  val itemSpacing: TextUnit? = null,\n  val orderedMarkers: (RichTextScope.() -> OrderedMarkers)? = null,\n  val unorderedMarkers: (RichTextScope.() -> UnorderedMarkers)? = null\n) {\n  public companion object {\n    public val Default: ListStyle = ListStyle()\n  }\n}\n\nprivate val DefaultMarkerIndent = 8.sp\nprivate val DefaultContentsIndent = 4.sp\nprivate val DefaultItemSpacing = 4.sp\nprivate val DefaultOrderedMarkers: RichTextScope.() -> OrderedMarkers = {\n  textOrderedMarkers(\n    { \"${it + 1}.\" },\n    {\n      ('a'..'z').drop(it % 26)\n        .first() + \".\"\n    },\n    { \"${it + 1})\" },\n    {\n      ('a'..'z').drop(it % 26)\n        .first() + \")\"\n    }\n  )\n}\nprivate val DefaultUnorderedMarkers: RichTextScope.() -> UnorderedMarkers = {\n  textUnorderedMarkers(\"•\", \"◦\", \"▸\", \"▹\")\n}\n\ninternal fun ListStyle.resolveDefaults(): ListStyle = ListStyle(\n  markerIndent = markerIndent ?: DefaultMarkerIndent,\n  contentsIndent = contentsIndent ?: DefaultContentsIndent,\n  itemSpacing = itemSpacing ?: DefaultItemSpacing,\n  orderedMarkers = orderedMarkers ?: DefaultOrderedMarkers,\n  unorderedMarkers = unorderedMarkers ?: DefaultUnorderedMarkers\n)\n\nprivate val LocalListLevel = compositionLocalOf { 0 }\n\n/**\n * Composes [children] with their [LocalListLevel] reset back to 0.\n */\n@Composable internal fun RestartListLevel(children: @Composable () -> Unit) {\n  CompositionLocalProvider(LocalListLevel provides 0) {\n    children()\n  }\n}\n\n/**\n * Creates a formatted list such as a bullet list or numbered list.\n */\n// inline is required for https://github.com/halilozercan/compose-richtext/issues/7\n@Suppress(\"NOTHING_TO_INLINE\")\n@Composable public inline fun RichTextScope.FormattedList(\n  listType: ListType,\n  vararg children: @Composable RichTextScope.() -> Unit\n): Unit = FormattedList(listType, children.asList(), 0) { it() }\n\n/**\n * Creates a formatted list such as a bullet list or numbered list.\n */\n@Composable public fun <T> RichTextScope.FormattedList(\n  listType: ListType,\n  items: List<T>,\n  startIndex: Int = 0,\n  drawItem: @Composable RichTextScope.(T) -> Unit\n) {\n  val listStyle = currentRichTextStyle.resolveDefaults().listStyle!!\n  val density = LocalDensity.current\n  val markerIndent = with(density) { listStyle.markerIndent!!.toDp() }\n  val contentsIndent = with(density) { listStyle.contentsIndent!!.toDp() }\n  val itemSpacing = with(density) { listStyle.itemSpacing!!.toDp() }\n  val currentLevel = LocalListLevel.current\n\n  PrefixListLayout(\n    count = items.size,\n    itemSpacing = itemSpacing,\n    prefixPadding = PaddingValues(start = markerIndent, end = contentsIndent),\n    prefixForIndex = { index ->\n      when (listType) {\n        Ordered -> listStyle.orderedMarkers!!().drawMarker(currentLevel, startIndex + index)\n        Unordered -> listStyle.unorderedMarkers!!().drawMarker(currentLevel)\n      }\n    },\n    itemForIndex = { index ->\n      BasicRichText(\n        style = currentRichTextStyle.copy(paragraphSpacing = listStyle.itemSpacing),\n      ) {\n        CompositionLocalProvider(LocalListLevel provides currentLevel + 1) {\n          drawItem(items[index])\n        }\n      }\n    }\n  )\n}\n\n@Composable private fun PrefixListLayout(\n  count: Int,\n  itemSpacing: Dp,\n  prefixPadding: PaddingValues,\n  prefixForIndex: @Composable (index: Int) -> Unit,\n  itemForIndex: @Composable (index: Int) -> Unit\n) {\n  Layout(content = {\n    // List markers aren't selectable.\n    DisableSelection {\n      // Draw the markers first.\n      for (i in 0 until count) {\n        // TODO Use the padding in the calculation directly instead of wrapping.\n        Box(Modifier.padding(prefixPadding)) {\n          prefixForIndex(i)\n        }\n      }\n    }\n\n    // Then draw the items.\n    for (i in 0 until count) {\n      itemForIndex(i)\n    }\n  }) { measurables, constraints ->\n    check(measurables.size == count * 2)\n    val prefixMeasureables = measurables.asSequence()\n      .take(count)\n    val itemMeasurables = measurables.asSequence()\n      .drop(count)\n\n    // Measure the prefixes first.\n    val prefixPlaceables = prefixMeasureables.map { marker ->\n      marker.measure(constraints)\n    }\n      .toList()\n    val widestPrefix = prefixPlaceables.maxByOrNull { it.width }!!\n\n    // Then measure the items, offset to the right to allow space for the prefixes and gap.\n    val itemConstraints = constraints.copy(\n      maxWidth = (constraints.maxWidth - widestPrefix.width).coerceAtLeast(0)\n    )\n    val itemPlaceables = itemMeasurables.map { item ->\n      item.measure(itemConstraints)\n    }\n      .toList()\n    val widestItem = itemPlaceables.maxByOrNull { it.width }!!\n\n    val listWidth = widestPrefix.width + widestItem.width\n    val itemsHeight = itemPlaceables.sumOf { it.height } +\n        (itemPlaceables.size - 1) * itemSpacing.roundToPx()\n    val prefixesHeight = prefixPlaceables.sumOf { it.height } +\n        (prefixPlaceables.size - 1) * itemSpacing.roundToPx()\n\n    val listHeight = maxOf(itemsHeight, prefixesHeight)\n    layout(listWidth, listHeight) {\n      var y = 0\n\n      // Flow the rows vertically, much like Column.\n      for (i in 0 until count) {\n        val prefix = prefixPlaceables[i]\n        val item = itemPlaceables[i]\n        val rowHeight = max(prefix.height, item.height) + itemSpacing.roundToPx()\n        val size = IntSize(\n          width = widestPrefix.width - prefix.width,\n          height = rowHeight - prefix.height\n        )\n        val prefixOffset = Alignment.TopEnd.align(\n          size = size,\n          space = size,\n          layoutDirection = layoutDirection\n        )\n\n        prefix.placeRelative(prefixOffset.x, y + prefixOffset.y)\n        item.placeRelative(widestPrefix.width, y)\n        y += rowHeight\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Heading.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.semantics.heading\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontStyle.Companion.Italic\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.resolveDefaults\nimport androidx.compose.ui.unit.sp\n\n\n/**\n * Function that computes the [TextStyle] for the given header level, given the current [TextStyle]\n * for this point in the composition. Note that the [TextStyle] passed into this function will be\n * fully resolved. The returned style will then be _merged_ with the passed-in text style, so any\n * unspecified properties will be inherited.\n */\n// TODO factor a generic \"block style\" thing out, use for code block, quote block, and this, to\n// also allow controlling top/bottom space.\npublic typealias HeadingStyle = (level: Int, textStyle: TextStyle) -> TextStyle\n\ninternal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->\n  when (level) {\n    0 -> TextStyle(\n        fontSize = 36.sp,\n        fontWeight = FontWeight.Bold\n    )\n    1 -> TextStyle(\n        fontSize = 26.sp,\n        fontWeight = FontWeight.Bold\n    )\n    2 -> TextStyle(\n        fontSize = 22.sp,\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .7F)\n    )\n    3 -> TextStyle(\n        fontSize = 20.sp,\n        fontWeight = FontWeight.Bold,\n        fontStyle = Italic\n    )\n    4 -> TextStyle(\n        fontSize = 18.sp,\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .7F)\n    )\n    5 -> TextStyle(\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .5f)\n    )\n    else -> textStyle\n  }\n}\n\n/**\n * A section heading.\n *\n * @param level The non-negative rank of the header, with 0 being the most important.\n */\n@Composable public fun RichTextScope.Heading(\n  level: Int,\n  text: String\n) {\n  Heading(level) {\n    Text(text, Modifier.semantics { heading() })\n  }\n}\n\n/**\n * A section heading.\n *\n * @param level The non-negative rank of the header, with 0 being the most important.\n */\n@Composable public fun RichTextScope.Heading(\n  level: Int,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  require(level >= 0) { \"Level must be at least 0\" }\n\n  val incomingStyle = currentTextStyle.let {\n    it.copy(color = it.color.takeOrElse { currentContentColor })\n  }\n  val currentTextStyle = resolveDefaults(incomingStyle, LocalLayoutDirection.current)\n\n  val headingStyleFunction = currentRichTextStyle.resolveDefaults().headingStyle!!\n  val headingTextStyle = headingStyleFunction(level, currentTextStyle)\n  val mergedTextStyle = currentTextStyle.merge(headingTextStyle)\n\n  textStyleBackProvider(mergedTextStyle) {\n    children()\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/HorizontalRule.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\n\n/**\n * A simple horizontal line drawn with the current content color.\n */\n@Composable public fun RichTextScope.HorizontalRule() {\n  val color = currentContentColor.copy(alpha = .2f)\n  val spacing = with(LocalDensity.current) {\n    currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp()\n  }\n  Box(\n    Modifier\n      .padding(top = spacing, bottom = spacing)\n      .fillMaxWidth()\n      .height(1.dp)\n      .background(color)\n  )\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/InfoPanel.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport com.halilibo.richtext.ui.InfoPanelType.Danger\nimport com.halilibo.richtext.ui.InfoPanelType.Primary\nimport com.halilibo.richtext.ui.InfoPanelType.Secondary\nimport com.halilibo.richtext.ui.InfoPanelType.Success\nimport com.halilibo.richtext.ui.InfoPanelType.Warning\n\n@Stable\npublic data class InfoPanelStyle(\n  val contentPadding: PaddingValues? = null,\n  val background: @Composable ((InfoPanelType) -> Modifier)? = null,\n  val textStyle: @Composable ((InfoPanelType) -> TextStyle)? = null\n) {\n  public companion object {\n    public val Default: InfoPanelStyle = InfoPanelStyle()\n  }\n}\n\npublic enum class InfoPanelType {\n  Primary,\n  Secondary,\n  Success,\n  Danger,\n  Warning\n}\n\nprivate val DefaultContentPadding = PaddingValues(8.dp)\nprivate val DefaultInfoPanelBackground = @Composable { infoPanelType: InfoPanelType ->\n  remember {\n    val (borderColor, backgroundColor) = when (infoPanelType) {\n      Primary -> Color(0xffb8daff) to Color(0xffcce5ff)\n      Secondary -> Color(0xffd6d8db) to Color(0xffe2e3e5)\n      Success -> Color(0xffc3e6cb) to Color(0xffd4edda)\n      Danger -> Color(0xfff5c6cb) to Color(0xfff8d7da)\n      Warning -> Color(0xffffeeba) to Color(0xfffff3cd)\n    }\n\n    Modifier\n      .border(1.dp, borderColor, RoundedCornerShape(4.dp))\n      .background(backgroundColor, RoundedCornerShape(4.dp))\n  }\n}\n\nprivate val DefaultInfoPanelTextStyle = @Composable { infoPanelType: InfoPanelType ->\n  remember {\n    val color = when(infoPanelType) {\n      Primary -> Color(0xff004085)\n      Secondary -> Color(0xff383d41)\n      Success -> Color(0xff155724)\n      Danger -> Color(0xff721c24)\n      Warning -> Color(0xff856404)\n    }\n    TextStyle(color = color)\n  }\n}\n\ninternal fun InfoPanelStyle.resolveDefaults() = InfoPanelStyle(\n  contentPadding = contentPadding ?: DefaultContentPadding,\n  background = background ?: DefaultInfoPanelBackground,\n  textStyle = textStyle ?: DefaultInfoPanelTextStyle\n)\n\n/**\n * A panel to show content similar to Bootstrap alerts, categorized as [InfoPanelType].\n * This composable is a shortcut to show only [text] in an info panel.\n */\n@Composable\npublic fun RichTextScope.InfoPanel(\n  infoPanelType: InfoPanelType,\n  text: String\n) {\n  InfoPanel(infoPanelType) {\n    Text(text)\n  }\n}\n\n/**\n * A panel to show content similar to Bootstrap alerts, categorized as [InfoPanelType].\n */\n@Composable\npublic fun RichTextScope.InfoPanel(\n  infoPanelType: InfoPanelType,\n  content: @Composable () -> Unit\n) {\n  val infoPanelStyle = currentRichTextStyle.resolveDefaults().infoPanelStyle!!\n  val backgroundModifier = infoPanelStyle.background!!.invoke(infoPanelType)\n  val infoPanelTextStyle = infoPanelStyle.textStyle!!.invoke(infoPanelType)\n\n  val resolvedTextStyle = currentTextStyle.merge(infoPanelTextStyle)\n\n  textStyleBackProvider(resolvedTextStyle) {\n    Box(modifier = backgroundModifier.padding(infoPanelStyle.contentPadding!!)) {\n      content()\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextLocals.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.halilibo.richtext.ui.util.detectTapGesturesIf\n\n/**\n * Carries the text style in Composition tree. [Heading], [CodeBlock],\n * [BlockQuote] are designed to change the ongoing [TextStyle] in composition,\n * so that their children can use the modified text style implicitly.\n *\n * LocalTextStyle also exists in Material package but this one is internal\n * to RichText.\n */\ninternal val LocalInternalTextStyle = compositionLocalOf { TextStyle.Default }\n\n/**\n * Carries the content color in Composition tree. Default TextStyle\n * does not have text color specified. It defaults to [Color.Black]\n * in the \"resolve chain\" but Dark Mode is an exception. To also resolve\n * for Dark Mode, content color should be passed to [RichTextScope].\n */\ninternal val LocalInternalContentColor = compositionLocalOf { Color.Black }\n\n/**\n * The current [TextStyle].\n */\ninternal val RichTextScope.currentTextStyle: TextStyle\n  @Composable get() = textStyleProvider()\n\n/**\n * The current content [Color].\n */\ninternal val RichTextScope.currentContentColor: Color\n  @Composable get() = contentColorProvider()\n\n/**\n * Intended for preview composables.\n */\n@Composable\ninternal fun RichTextScope.Text(\n  text: String,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  overflow: TextOverflow = TextOverflow.Clip,\n  softWrap: Boolean = true,\n  maxLines: Int = Int.MAX_VALUE\n) {\n  val textColor = currentTextStyle.color.takeOrElse { currentContentColor }\n  val style = currentTextStyle.copy(color = textColor)\n\n  BasicText(\n    text = text,\n    modifier = modifier,\n    style = style,\n    onTextLayout = onTextLayout,\n    overflow = overflow,\n    softWrap = softWrap,\n    maxLines = maxLines\n  )\n}\n\n@Composable\ninternal fun RichTextScope.Text(\n  text: AnnotatedString,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  overflow: TextOverflow = TextOverflow.Clip,\n  softWrap: Boolean = true,\n  maxLines: Int = Int.MAX_VALUE,\n  inlineContent: Map<String, InlineTextContent> = mapOf(),\n) {\n  val textColor = currentTextStyle.color.takeOrElse { currentContentColor }\n  val style = currentTextStyle.copy(color = textColor)\n\n  BasicText(\n    text = text,\n    modifier = modifier,\n    style = style,\n    onTextLayout = onTextLayout,\n    overflow = overflow,\n    softWrap = softWrap,\n    maxLines = maxLines,\n    inlineContent = inlineContent\n  )\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextScope.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.CompositionLocal\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.State\n\n/**\n * Scope object for composables that can draw rich text.\n *\n * RichTextScope facilitates a context for RichText elements. It does not\n * behave like a [State] or a [CompositionLocal]. Starting from [BasicRichText],\n * this scope carries information that should not be passed down as a state.\n */\n@Immutable\npublic object RichTextScope\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextStyle.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.string.RichTextStringStyle\n\ninternal val LocalRichTextStyle = compositionLocalOf { RichTextStyle.Default }\ninternal val DefaultParagraphSpacing: TextUnit = 8.sp\n\n/**\n * Configures all formatting attributes for drawing rich text.\n *\n * @param paragraphSpacing The amount of space in between blocks of text.\n * @param headingStyle The [HeadingStyle] that defines how [Heading]s are drawn.\n * @param listStyle The [ListStyle] used to format [FormattedList]s.\n * @param blockQuoteGutter The [BlockQuoteGutter] used to draw [BlockQuote]s.\n * @param codeBlockStyle The [CodeBlockStyle] that defines how [CodeBlock]s are drawn.\n * @param tableStyle The [TableStyle] used to render [Table]s.\n * @param stringStyle The [RichTextStringStyle] used to render\n * [RichTextString][com.halilibo.richtext.ui.string.RichTextString]s\n */\n@Immutable\npublic data class RichTextStyle(\n  val paragraphSpacing: TextUnit? = null,\n  val headingStyle: HeadingStyle? = null,\n  val listStyle: ListStyle? = null,\n  val blockQuoteGutter: BlockQuoteGutter? = null,\n  val codeBlockStyle: CodeBlockStyle? = null,\n  val tableStyle: TableStyle? = null,\n  val infoPanelStyle: InfoPanelStyle? = null,\n  val stringStyle: RichTextStringStyle? = null\n) {\n  public companion object {\n    public val Default: RichTextStyle = RichTextStyle()\n  }\n}\n\npublic fun RichTextStyle.merge(otherStyle: RichTextStyle?): RichTextStyle = RichTextStyle(\n  paragraphSpacing = otherStyle?.paragraphSpacing ?: paragraphSpacing,\n  headingStyle = otherStyle?.headingStyle ?: headingStyle,\n  listStyle = otherStyle?.listStyle ?: listStyle,\n  blockQuoteGutter = otherStyle?.blockQuoteGutter ?: blockQuoteGutter,\n  codeBlockStyle = otherStyle?.codeBlockStyle ?: codeBlockStyle,\n  tableStyle = otherStyle?.tableStyle ?: tableStyle,\n  infoPanelStyle = otherStyle?.infoPanelStyle ?: infoPanelStyle,\n  stringStyle = stringStyle?.merge(otherStyle?.stringStyle) ?: otherStyle?.stringStyle\n)\n\npublic fun RichTextStyle.resolveDefaults(): RichTextStyle = RichTextStyle(\n  paragraphSpacing = paragraphSpacing ?: DefaultParagraphSpacing,\n  headingStyle = headingStyle ?: DefaultHeadingStyle,\n  listStyle = (listStyle ?: ListStyle.Default).resolveDefaults(),\n  blockQuoteGutter = blockQuoteGutter ?: DefaultBlockQuoteGutter,\n  codeBlockStyle = (codeBlockStyle ?: CodeBlockStyle.Default).resolveDefaults(),\n  tableStyle = (tableStyle ?: TableStyle.Default).resolveDefaults(),\n  infoPanelStyle = (infoPanelStyle ?: InfoPanelStyle.Default).resolveDefaults(),\n  stringStyle = (stringStyle ?: RichTextStringStyle.Default).resolveDefaults()\n)\n\n/**\n * The current [RichTextStyle].\n */\npublic val RichTextScope.currentRichTextStyle: RichTextStyle\n  @Composable get() = LocalRichTextStyle.current\n\n/**\n * Sets the [RichTextStyle] for its [children].\n */\n@Composable\npublic fun RichTextScope.WithStyle(\n  style: RichTextStyle?,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  if (style == null) {\n    children()\n  } else {\n    val mergedStyle = LocalRichTextStyle.current.merge(style)\n    CompositionLocalProvider(LocalRichTextStyle provides mergedStyle) {\n      children()\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeConfiguration.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\n\ninternal typealias TextStyleProvider = @Composable () -> TextStyle\ninternal typealias TextStyleBackProvider = @Composable (TextStyle, @Composable () -> Unit) -> Unit\ninternal typealias ContentColorProvider = @Composable () -> Color\ninternal typealias ContentColorBackProvider = @Composable (Color, @Composable () -> Unit) -> Unit\n\ninternal data class RichTextThemeConfiguration(\n  val textStyleProvider: TextStyleProvider = { LocalInternalTextStyle.current },\n  val textStyleBackProvider: TextStyleBackProvider = { newTextStyle, content ->\n    CompositionLocalProvider(LocalInternalTextStyle provides newTextStyle) {\n      content()\n    }\n  },\n  val contentColorProvider: ContentColorProvider = { LocalInternalContentColor.current },\n  val contentColorBackProvider: ContentColorBackProvider = { newColor, content ->\n    CompositionLocalProvider(LocalInternalContentColor provides newColor) {\n      content()\n    }\n  }\n) {\n  companion object {\n    internal val Default = RichTextThemeConfiguration()\n  }\n}\n\ninternal val LocalRichTextThemeConfiguration: ProvidableCompositionLocal<RichTextThemeConfiguration> =\n  compositionLocalOf { RichTextThemeConfiguration() }\n\n/**\n * Easy access delegations for [RichTextThemeProvider] within [RichTextScope]\n */\ninternal val RichTextScope.textStyleProvider: @Composable () -> TextStyle\n  @Composable get() = LocalRichTextThemeConfiguration.current.textStyleProvider\n\ninternal val RichTextScope.textStyleBackProvider: @Composable (TextStyle, @Composable () -> Unit) -> Unit\n  @Composable get() = LocalRichTextThemeConfiguration.current.textStyleBackProvider\n\ninternal val RichTextScope.contentColorProvider: @Composable () -> Color\n  @Composable get() = LocalRichTextThemeConfiguration.current.contentColorProvider\n\ninternal val RichTextScope.contentColorBackProvider: @Composable (Color, @Composable () -> Unit) -> Unit\n  @Composable get() = LocalRichTextThemeConfiguration.current.contentColorBackProvider"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeProvider.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\n\n/**\n * Entry point for integrating app's own typography and theme system with RichText.\n *\n * API for this integration is highly influenced by how compose-material theming\n * is designed. RichText library assumes that almost all Theme/Design systems would\n * have composition locals that provide text style downstream.\n *\n * Moreover, text style should not include text color by best practice. Content color\n * exists to figure text color in current context. Light/Dark theming leverages content\n * color to influence not just text but other parts of theming as well.\n *\n * @param textStyleProvider Returns the current text style.\n * @param textStyleBackProvider RichText sometimes updates the current text style\n * e.g. Heading, CodeBlock, and etc. New style should be passed to the outer\n * theming to indicate that there is a need for update, so that children Text\n * composables use the correct styling.\n * @param contentColorProvider Returns the current content color.\n * @param contentColorBackProvider Similar to [textStyleBackProvider], does the same job\n * for content color.\n */\n@Composable\npublic fun RichTextThemeProvider(\n  textStyleProvider: @Composable (() -> TextStyle)? = null,\n  textStyleBackProvider: @Composable ((TextStyle, @Composable () -> Unit) -> Unit)? = null,\n  contentColorProvider: @Composable (() -> Color)? = null,\n  contentColorBackProvider: @Composable ((Color, @Composable () -> Unit) -> Unit)? = null,\n  content: @Composable () -> Unit\n) {\n  CompositionLocalProvider(\n    LocalRichTextThemeConfiguration provides\n        RichTextThemeConfiguration(\n          textStyleProvider = textStyleProvider\n            ?: RichTextThemeConfiguration.Default.textStyleProvider,\n          textStyleBackProvider = textStyleBackProvider\n            ?: RichTextThemeConfiguration.Default.textStyleBackProvider,\n          contentColorProvider = contentColorProvider\n            ?: RichTextThemeConfiguration.Default.contentColorProvider,\n          contentColorBackProvider = contentColorBackProvider\n            ?: RichTextThemeConfiguration.Default.contentColorBackProvider,\n        )\n  ) {\n    content()\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/SimpleTableLayout.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.constrain\nimport kotlin.math.roundToInt\n\n/**\n * The offsets of rows and columns of a [SimpleTableLayout], centered inside their spacing.\n *\n * E.g. If a table is given a cell spacing of 2px, then the first column and row offset will each\n * be 1px.\n */\n@Immutable\ninternal data class TableLayoutResult(\n  val rowOffsets: List<Float>,\n  val columnOffsets: List<Float>\n)\n\n/**\n * A simple table that sizes all columns equally.\n *\n * @param cellSpacing The space in between each cell, and between each outer cell and the edge of\n * the table.\n */\n@OptIn(ExperimentalStdlibApi::class)\n@Composable\ninternal fun SimpleTableLayout(\n  columns: Int,\n  rows: List<List<@Composable () -> Unit>>,\n  drawDecorations: (TableLayoutResult) -> Modifier,\n  cellSpacing: Float,\n  modifier: Modifier\n) {\n  SubcomposeLayout(modifier = modifier) { constraints ->\n    val measurables = subcompose(false) {\n      rows.forEach { row ->\n        check(row.size == columns)\n        row.forEach { cell ->\n          cell()\n        }\n      }\n    }\n\n    val rowMeasurables = measurables.chunked(columns)\n    check(rowMeasurables.size == rows.size)\n\n    check(constraints.hasBoundedWidth) { \"Table must have bounded width\" }\n    // Divide the width by the number of columns, then leave room for the padding.\n    val cellSpacingWidth = cellSpacing * (columns + 1)\n    val cellWidth = (constraints.maxWidth - cellSpacingWidth) / columns\n    val cellSpacingHeight = cellSpacing * (rowMeasurables.size + 1)\n    // TODO Handle bounded height constraints.\n    // val cellMaxHeight = if (!constraints.hasBoundedHeight) {\n    //   Float.MAX_VALUE\n    // } else {\n    //   // Divide the height by the number of rows, then leave room for the padding.\n    //   (constraints.maxHeight - cellSpacingHeight) / rowMeasurables.size\n    // }\n    val cellConstraints = Constraints(maxWidth = cellWidth.roundToInt()).constrain(constraints)\n\n    val rowPlaceables = rowMeasurables.map { cellMeasurables ->\n      cellMeasurables.map { cell ->\n        cell.measure(cellConstraints)\n      }\n    }\n    val rowHeights = rowPlaceables.map { row -> row.maxByOrNull { it.height }!!.height }\n\n    val tableWidth = constraints.maxWidth\n    val tableHeight = (rowHeights.sumOf { it } + cellSpacingHeight).roundToInt()\n    layout(tableWidth, tableHeight) {\n      var y = cellSpacing\n      val rowOffsets = mutableListOf<Float>()\n      val columnOffsets = mutableListOf<Float>()\n\n      rowPlaceables.forEachIndexed { rowIndex, cellPlaceables ->\n        rowOffsets += y - cellSpacing / 2f\n        var x = cellSpacing\n\n        cellPlaceables.forEach { cell ->\n          if (rowIndex == 0) {\n            columnOffsets.add(x - cellSpacing / 2f)\n          }\n          cell.place(x.roundToInt(), y.roundToInt())\n          x += cellWidth + cellSpacing\n        }\n\n        if (rowIndex == 0) {\n          // Add the right-most edge.\n          columnOffsets.add(x - cellSpacing / 2f)\n        }\n\n        y += rowHeights[rowIndex] + cellSpacing\n      }\n\n      rowOffsets.add(y - cellSpacing / 2f)\n\n      // Compose and draw the borders.\n      val layoutResult = TableLayoutResult(rowOffsets, columnOffsets)\n      subcompose(true) {\n        Box(modifier = drawDecorations(layoutResult))\n      }.single()\n        .measure(Constraints.fixed(tableWidth, tableHeight))\n        .placeRelative(0, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Table.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport kotlin.math.max\n\n/**\n * Defines the visual style for a [Table].\n *\n * @param headerTextStyle The [TextStyle] used for header rows.\n * @param cellPadding The spacing between the contents of each cell and the borders.\n * @param borderColor The [Color] of the table border.\n * @param borderStrokeWidth The width of the table border.\n */\n@Immutable\npublic data class TableStyle(\n  val headerTextStyle: TextStyle? = null,\n  val cellPadding: TextUnit? = null,\n  val borderColor: Color? = null,\n  val borderStrokeWidth: Float? = null\n) {\n  public companion object {\n    public val Default: TableStyle = TableStyle()\n  }\n}\n\nprivate val DefaultTableHeaderTextStyle = TextStyle(fontWeight = FontWeight.Bold)\nprivate val DefaultCellPadding = 8.sp\nprivate val DefaultBorderColor = Color.Unspecified\nprivate const val DefaultBorderStrokeWidth = 1f\n\ninternal fun TableStyle.resolveDefaults() = TableStyle(\n    headerTextStyle = headerTextStyle ?: DefaultTableHeaderTextStyle,\n    cellPadding = cellPadding ?: DefaultCellPadding,\n    borderColor = borderColor ?: DefaultBorderColor,\n    borderStrokeWidth = borderStrokeWidth ?: DefaultBorderStrokeWidth\n)\n\npublic interface RichTextTableRowScope {\n  public fun row(children: RichTextTableCellScope.() -> Unit)\n}\n\npublic interface RichTextTableCellScope {\n  public fun cell(children: @Composable RichTextScope.() -> Unit)\n}\n\n@Immutable\nprivate data class TableRow(val cells: List<@Composable RichTextScope.() -> Unit>)\n\nprivate class TableBuilder : RichTextTableRowScope {\n  val rows = mutableListOf<RowBuilder>()\n\n  override fun row(children: RichTextTableCellScope.() -> Unit) {\n    rows += RowBuilder().apply(children)\n  }\n}\n\nprivate class RowBuilder : RichTextTableCellScope {\n  var row = TableRow(emptyList())\n\n  override fun cell(children: @Composable RichTextScope.() -> Unit) {\n    row = TableRow(row.cells + children)\n  }\n}\n\n/**\n * Draws a table with an optional header row, and an arbitrary number of body rows.\n *\n * The style of the table is defined by the [RichTextStyle.tableStyle]&nbsp;[TableStyle].\n */\n@OptIn(ExperimentalStdlibApi::class)\n@Composable\npublic fun RichTextScope.Table(\n  modifier: Modifier = Modifier,\n  headerRow: (RichTextTableCellScope.() -> Unit)? = null,\n  bodyRows: RichTextTableRowScope.() -> Unit\n) {\n  val tableStyle = currentRichTextStyle.resolveDefaults().tableStyle!!\n  val contentColor = currentContentColor\n  val header = remember(headerRow) {\n    headerRow?.let { RowBuilder().apply(headerRow).row }\n  }\n  val rows = remember(bodyRows) {\n    TableBuilder().apply(bodyRows).rows.map { it.row }\n  }\n  val columns = remember(header, rows) {\n    max(\n        header?.cells?.size ?: 0,\n        rows.maxByOrNull { it.cells.size }?.cells?.size ?: 0\n    )\n  }\n  val headerStyle = currentTextStyle.merge(tableStyle.headerTextStyle)\n  val cellPadding = with(LocalDensity.current) {\n    tableStyle.cellPadding!!.toDp()\n  }\n  val cellModifier = Modifier\n      .clipToBounds()\n      .padding(cellPadding)\n\n  val styledRows = remember(header, rows, cellModifier) {\n    buildList {\n      header?.let { headerRow ->\n        // Type inference seems to puke without explicit parameters.\n        @Suppress(\"RemoveExplicitTypeArguments\")\n        add(headerRow.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell ->\n          @Composable {\n            textStyleBackProvider(headerStyle) {\n              BasicRichText(\n                modifier = cellModifier,\n                children = cell\n              )\n            }\n          }\n        })\n      }\n\n      rows.mapTo(this) { row ->\n        @Suppress(\"RemoveExplicitTypeArguments\")\n        row.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell ->\n          @Composable {\n            BasicRichText(\n              modifier = cellModifier,\n              children = cell\n            )\n          }\n        }\n      }\n    }\n  }\n\n  // For some reason borders don't get drawn in the Preview, but they work on-device.\n  SimpleTableLayout(\n      columns = columns,\n      rows = styledRows,\n      cellSpacing = tableStyle.borderStrokeWidth!!,\n      drawDecorations = { layoutResult ->\n        Modifier.drawTableBorders(\n            rowOffsets = layoutResult.rowOffsets,\n            columnOffsets = layoutResult.columnOffsets,\n            borderColor = tableStyle.borderColor!!.takeOrElse { contentColor },\n            borderStrokeWidth = tableStyle.borderStrokeWidth\n        )\n      },\n      modifier = modifier\n  )\n}\n\nprivate fun Modifier.drawTableBorders(\n  rowOffsets: List<Float>,\n  columnOffsets: List<Float>,\n  borderColor: Color,\n  borderStrokeWidth: Float\n) = drawBehind {\n  // Draw horizontal borders.\n  rowOffsets.forEach { position ->\n    drawLine(\n        borderColor,\n        start = Offset(0f, position),\n        end = Offset(size.width, position),\n        borderStrokeWidth\n    )\n  }\n\n  // Draw vertical borders.\n  columnOffsets.forEach { position ->\n    drawLine(\n        borderColor,\n        Offset(position, 0f),\n        Offset(position, size.height),\n        borderStrokeWidth\n    )\n  }\n}"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/InlineContent.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"FunctionName\")\n\npackage com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.structuralEqualityPolicy\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.PlaceholderVerticalAlign.Companion.AboveBaseline\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.sp\n\n/**\n * A Composable that can be embedded inline in a [RichTextString] by passing to\n * [RichTextString.Builder.appendInlineContent].\n *\n * @param initialSize Optional function to calculate the initial size of the content. Not specifying\n * this may cause flicker.\n * @param placeholderVerticalAlign Used to specify how a placeholder is vertically aligned within a\n * text line.\n */\npublic class InlineContent(\n  internal val initialSize: (Density.() -> IntSize)? = null,\n  internal val placeholderVerticalAlign: PlaceholderVerticalAlign = AboveBaseline,\n  internal val content: @Composable Density.(alternateText: String) -> Unit\n)\n\n/**\n * Converts a map of [InlineContent]s into a map of [InlineTextContent] that is ready to pass to\n * the core Text composable. Whenever any of the contents resize themselves, or if the map changes,\n * a new map will be returned with updated [Placeholder]s.\n */\n@Composable internal fun manageInlineTextContents(\n  inlineContents: Map<String, InlineContent>,\n  textConstraints: Constraints\n): Map<String, InlineTextContent> {\n  val density = LocalDensity.current\n\n  return inlineContents.mapValues { (_, content) ->\n    reifyInlineContent(\n      content,\n      Constraints(maxWidth = textConstraints.maxWidth, maxHeight = textConstraints.maxHeight),\n      density\n    )\n  }\n}\n\n/**\n * Given an [InlineContent] function, wraps it in a [InlineTextContent] that will allow the content\n * to measure itself inside the enclosing layout's maximum constraints, and automatically return a\n * new [InlineTextContent] whenever the content changes size to update how much space is reserved\n * in the text layout for the content.\n */\n@Composable private fun reifyInlineContent(\n  content: InlineContent,\n  contentConstraints: Constraints,\n  density: Density\n): InlineTextContent {\n  var size by remember {\n    mutableStateOf(\n      content.initialSize?.invoke(density),\n      structuralEqualityPolicy()\n    )\n  }\n\n  with(density) {\n    // If size is null, content hasn't been measured yet, so just draw with zero width for now.\n    // Set the height to 1 em so we can calculate how many pixels in an EM.\n    val placeholder = Placeholder(\n      width = size?.width?.toSp() ?: 0.sp,\n      height = size?.height?.toSp() ?: 1.sp,\n      placeholderVerticalAlign = content.placeholderVerticalAlign\n    )\n\n    return InlineTextContent(placeholder) { alternateText ->\n      Layout(content = { content.content(this, alternateText) }) { measurables, _ ->\n        // Measure the content with the constraints for the parent Text layout, not the actual.\n        // This allows it to determine exactly how large it needs to be so we can update the\n        // placeholder.\n        val contentPlaceable = measurables.singleOrNull()?.measure(contentConstraints)\n          ?: return@Layout layout(0, 0) {}\n\n        if (contentPlaceable.width != size?.width\n          || contentPlaceable.height != size?.height\n        ) {\n          size = IntSize(contentPlaceable.width, contentPlaceable.height)\n        }\n\n        layout(contentPlaceable.width, contentPlaceable.height) {\n          contentPlaceable.place(0, 0)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/RichTextString.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"SuspiciousCollectionReassignment\")\n\npackage com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.LinkInteractionListener\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.BaselineShift\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.DefaultCodeBlockBackgroundColor\nimport com.halilibo.richtext.ui.string.RichTextString.Builder\nimport com.halilibo.richtext.ui.string.RichTextString.Format\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Bold\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Code\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Companion.FormatAnnotationScope\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Italic\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Link\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Subscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Superscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Underline\nimport com.halilibo.richtext.ui.util.randomUUID\nimport kotlin.LazyThreadSafetyMode.NONE\n\n/** Copied from inline content. */\n@PublishedApi\ninternal const val REPLACEMENT_CHAR: String = \"\\uFFFD\"\n\n/**\n * Defines the [SpanStyle]s that are used for various [RichTextString] formatting directives.\n */\n@Immutable\npublic class RichTextStringStyle(\n  public val boldStyle: SpanStyle? = null,\n  public val italicStyle: SpanStyle? = null,\n  public val underlineStyle: SpanStyle? = null,\n  public val strikethroughStyle: SpanStyle? = null,\n  public val subscriptStyle: SpanStyle? = null,\n  public val superscriptStyle: SpanStyle? = null,\n  public val codeStyle: SpanStyle? = null,\n  public val linkStyle: TextLinkStyles? = null\n) {\n  public fun copy(\n    boldStyle: SpanStyle? = this.boldStyle,\n    italicStyle: SpanStyle? = this.italicStyle,\n    underlineStyle: SpanStyle? = this.underlineStyle,\n    strikethroughStyle: SpanStyle? = this.strikethroughStyle,\n    subscriptStyle: SpanStyle? = this.subscriptStyle,\n    superscriptStyle: SpanStyle? = this.superscriptStyle,\n    codeStyle: SpanStyle? = this.codeStyle,\n    linkStyle: TextLinkStyles? = this.linkStyle\n  ): RichTextStringStyle = RichTextStringStyle(\n    boldStyle = boldStyle,\n    italicStyle = italicStyle,\n    underlineStyle = underlineStyle,\n    strikethroughStyle = strikethroughStyle,\n    subscriptStyle = subscriptStyle,\n    superscriptStyle = superscriptStyle,\n    codeStyle = codeStyle,\n    linkStyle = linkStyle\n  )\n\n  internal fun merge(otherStyle: RichTextStringStyle?): RichTextStringStyle {\n    if (otherStyle == null) return this\n    return RichTextStringStyle(\n      boldStyle = boldStyle.merge(otherStyle.boldStyle),\n      italicStyle = italicStyle.merge(otherStyle.italicStyle),\n      underlineStyle = underlineStyle.merge(otherStyle.underlineStyle),\n      strikethroughStyle = strikethroughStyle.merge(otherStyle.strikethroughStyle),\n      subscriptStyle = subscriptStyle.merge(otherStyle.subscriptStyle),\n      superscriptStyle = superscriptStyle.merge(otherStyle.superscriptStyle),\n      codeStyle = codeStyle.merge(otherStyle.codeStyle),\n      linkStyle = linkStyle?.merge(otherStyle.linkStyle) ?: otherStyle.linkStyle\n    )\n  }\n\n  internal fun resolveDefaults(): RichTextStringStyle =\n    RichTextStringStyle(\n      boldStyle = boldStyle ?: Bold.DefaultStyle,\n      italicStyle = italicStyle ?: Italic.DefaultStyle,\n      underlineStyle = underlineStyle ?: Underline.DefaultStyle,\n      strikethroughStyle = strikethroughStyle ?: Strikethrough.DefaultStyle,\n      subscriptStyle = subscriptStyle ?: Subscript.DefaultStyle,\n      superscriptStyle = superscriptStyle ?: Superscript.DefaultStyle,\n      codeStyle = codeStyle ?: Code.DefaultStyle,\n      linkStyle = linkStyle ?: Link.DefaultStyle\n    )\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is RichTextStringStyle) return false\n\n    if (boldStyle != other.boldStyle) return false\n    if (italicStyle != other.italicStyle) return false\n    if (underlineStyle != other.underlineStyle) return false\n    if (strikethroughStyle != other.strikethroughStyle) return false\n    if (subscriptStyle != other.subscriptStyle) return false\n    if (superscriptStyle != other.superscriptStyle) return false\n    if (codeStyle != other.codeStyle) return false\n    if (linkStyle != other.linkStyle) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = boldStyle?.hashCode() ?: 0\n    result = 31 * result + (italicStyle?.hashCode() ?: 0)\n    result = 31 * result + (underlineStyle?.hashCode() ?: 0)\n    result = 31 * result + (strikethroughStyle?.hashCode() ?: 0)\n    result = 31 * result + (subscriptStyle?.hashCode() ?: 0)\n    result = 31 * result + (superscriptStyle?.hashCode() ?: 0)\n    result = 31 * result + (codeStyle?.hashCode() ?: 0)\n    result = 31 * result + (linkStyle?.hashCode() ?: 0)\n    return result\n  }\n\n  override fun toString(): String {\n    return \"RichTextStringStyle(boldStyle=$boldStyle, \" +\n        \"italicStyle=$italicStyle, \" +\n        \"underlineStyle=$underlineStyle, \" +\n        \"strikethroughStyle=$strikethroughStyle, \" +\n        \"subscriptStyle=$subscriptStyle, \" +\n        \"superscriptStyle=$superscriptStyle, \" +\n        \"codeStyle=$codeStyle, \" +\n        \"linkStyle=$linkStyle)\"\n  }\n\n  public companion object {\n    public val Default: RichTextStringStyle = RichTextStringStyle()\n\n    private fun SpanStyle?.merge(otherStyle: SpanStyle?): SpanStyle? =\n      this?.merge(otherStyle) ?: otherStyle\n  }\n}\n\n/**\n * Convenience function for creating a [RichTextString] using a [Builder].\n */\npublic inline fun richTextString(builder: Builder.() -> Unit): RichTextString =\n  Builder().apply(builder)\n    .toRichTextString()\n\n/**\n * A special type of [AnnotatedString] that is formatted using higher-level directives that are\n * configured using a [RichTextStringStyle].\n */\n@Immutable\npublic class RichTextString internal constructor(\n  private val taggedString: AnnotatedString,\n  internal val formatObjects: Map<String, Any>\n) {\n  private val length: Int get() = taggedString.length\n  public val text: String get() = taggedString.text\n\n  public operator fun plus(other: RichTextString): RichTextString =\n    Builder(length + other.length).run {\n      append(this@RichTextString)\n      append(other)\n      toRichTextString()\n    }\n\n  internal fun toAnnotatedString(\n    style: RichTextStringStyle,\n    contentColor: Color\n  ): AnnotatedString =\n    buildAnnotatedString {\n      append(taggedString)\n\n      // Get all of our format annotations.\n      val tags = taggedString.getStringAnnotations(FormatAnnotationScope, 0, taggedString.length)\n      // And apply their actual SpanStyles to the string.\n      tags.forEach { range ->\n        val format = Format.findTag(range.item, formatObjects) ?: return@forEach\n        format.getAnnotation(style, contentColor)\n          ?.let { annotation ->\n            if (annotation is SpanStyle) {\n              addStyle(annotation, range.start, range.end)\n            } else if (annotation is LinkAnnotation.Url) {\n              addLink(annotation, range.start, range.end)\n            }\n          }\n      }\n    }\n\n  internal fun getInlineContents(): Map<String, InlineContent> =\n    formatObjects.asSequence()\n      .mapNotNull { (tag, format) ->\n        tag.removePrefix(\"inline:\")\n          // If no prefix was found then we ignore it.\n          .takeUnless { it === tag }\n          ?.let {\n            Pair(it, format as InlineContent)\n          }\n      }\n      .toMap()\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is RichTextString) return false\n\n    if (taggedString != other.taggedString) return false\n    if (formatObjects != other.formatObjects) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = taggedString.hashCode()\n    result = 31 * result + formatObjects.hashCode()\n    return result\n  }\n\n  public sealed class Format(private val simpleTag: String? = null) {\n\n    /**\n     * This function should either return [SpanStyle] or [LinkAnnotation.Url]. In future releases of\n     * Compose these classes will have a common supertype called `AnnotatedString.Annotation`. Then\n     * we can stop returning [Any].\n     */\n    internal open fun getAnnotation(\n      richTextStyle: RichTextStringStyle,\n      contentColor: Color\n    ): Any? = null\n\n    public object Italic : Format(\"italic\") {\n      internal val DefaultStyle = SpanStyle(fontStyle = FontStyle.Italic)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.italicStyle\n    }\n\n    public object Bold : Format(simpleTag = \"foo\") {\n      internal val DefaultStyle = SpanStyle(fontWeight = FontWeight.Bold)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.boldStyle\n    }\n\n    public object Underline : Format(\"underline\") {\n      internal val DefaultStyle = SpanStyle(textDecoration = TextDecoration.Underline)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.underlineStyle\n    }\n\n    public object Strikethrough : Format(\"strikethrough\") {\n      internal val DefaultStyle = SpanStyle(textDecoration = TextDecoration.LineThrough)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.strikethroughStyle\n    }\n\n    public object Subscript : Format(\"subscript\") {\n      internal val DefaultStyle = SpanStyle(\n        baselineShift = BaselineShift(-0.2f),\n        // TODO this should be relative to current font size\n        fontSize = 10.sp\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.subscriptStyle\n    }\n\n    public object Superscript : Format(\"superscript\") {\n      internal val DefaultStyle = SpanStyle(\n        baselineShift = BaselineShift.Superscript,\n        fontSize = 10.sp\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.superscriptStyle\n    }\n\n    public object Code : Format(\"code\") {\n      internal val DefaultStyle = SpanStyle(\n        fontFamily = FontFamily.Monospace,\n        fontWeight = FontWeight.Medium,\n        background = DefaultCodeBlockBackgroundColor\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.codeStyle\n    }\n\n    public class Link(\n      public val destination: String,\n      public val linkInteractionListener: LinkInteractionListener? = null\n    ) : Format() {\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = LinkAnnotation.Url(\n        url = destination,\n        styles = richTextStyle.linkStyle,\n        linkInteractionListener = linkInteractionListener\n      )\n\n      override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is Link) return false\n\n        if (destination != other.destination) return false\n        if (linkInteractionListener != other.linkInteractionListener) return false\n\n        return true\n      }\n\n      override fun hashCode(): Int {\n        var result = destination.hashCode()\n        result = 31 * result + linkInteractionListener.hashCode()\n        return result\n      }\n\n      override fun toString(): String {\n        return \"Link(destination='$destination', linkInteractionListener=$linkInteractionListener)\"\n      }\n\n      internal companion object {\n        val DefaultStyle = TextLinkStyles(\n          style = SpanStyle(color = Color.Blue),\n          hoveredStyle = SpanStyle(\n            textDecoration = TextDecoration.Underline,\n            color = Color.Blue\n          )\n        )\n      }\n    }\n\n    internal fun registerTag(tags: MutableMap<String, Any>): String {\n      simpleTag?.let { return it }\n      val uuid = randomUUID()\n      tags[uuid] = this\n      return \"format:$uuid\"\n    }\n\n    internal companion object {\n      val FormatAnnotationScope = Format::class.qualifiedName!!\n\n      // For some reason, if this isn't lazy, Bold will always be null. Is Compose messing up static\n      // initialization order?\n      private val simpleTags by lazy(NONE) {\n        listOf(Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Code)\n      }\n\n      fun findTag(\n        tag: String,\n        tags: Map<String, Any>\n      ): Format? {\n        val stripped = tag.removePrefix(\"format:\")\n        return if (stripped === tag) {\n          // If the original string was returned, it means the string did not have the prefix.\n          simpleTags.firstOrNull { it.simpleTag == tag }\n        } else {\n          tags[stripped] as? Format\n        }\n      }\n    }\n  }\n\n  public class Builder(capacity: Int = 16) {\n    private val builder = AnnotatedString.Builder(capacity)\n    private val formatObjects = mutableMapOf<String, Any>()\n\n    public fun addFormat(\n      format: Format,\n      start: Int,\n      end: Int\n    ) {\n      val tag = format.registerTag(formatObjects)\n      builder.addStringAnnotation(FormatAnnotationScope, tag, start, end)\n    }\n\n    public fun pushFormat(format: Format): Int {\n      val tag = format.registerTag(formatObjects)\n      return builder.pushStringAnnotation(FormatAnnotationScope, tag)\n    }\n\n    public fun pop(): Unit = builder.pop()\n\n    public fun pop(index: Int): Unit = builder.pop(index)\n\n    public fun append(text: String): Unit = builder.append(text)\n\n    public fun append(text: RichTextString) {\n      builder.append(text.taggedString)\n      formatObjects.putAll(text.formatObjects)\n    }\n\n    public fun appendInlineContent(\n      alternateText: String = REPLACEMENT_CHAR,\n      content: InlineContent\n    ) {\n      val tag = randomUUID()\n      formatObjects[\"inline:$tag\"] = content\n      builder.appendInlineContent(tag, alternateText)\n    }\n\n    /**\n     * Provides access to the underlying builder, which can be used to add arbitrary formatting,\n     * including mixed with formatting from this Builder.\n     */\n    public fun <T> withAnnotatedString(block: AnnotatedString.Builder.() -> T): T = builder.block()\n\n    public fun toRichTextString(): RichTextString =\n      RichTextString(\n        builder.toAnnotatedString(),\n        formatObjects.toMap()\n      )\n  }\n}\n\npublic inline fun Builder.withFormat(\n  format: Format,\n  block: Builder.() -> Unit\n) {\n  val index = pushFormat(format)\n  block()\n  pop(index)\n}\n\nprivate fun TextLinkStyles.merge(other: TextLinkStyles?): TextLinkStyles {\n  return if (other == null) {\n    TextLinkStyles()\n  } else {\n    TextLinkStyles(\n      style = this.style?.merge(other.style) ?: other.style,\n      focusedStyle = this.style?.merge(other.focusedStyle) ?: other.focusedStyle,\n      hoveredStyle = this.style?.merge(other.hoveredStyle) ?: other.hoveredStyle,\n      pressedStyle = this.style?.merge(other.pressedStyle) ?: other.pressedStyle,\n    )\n  }\n}"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/Text.kt",
    "content": "package com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.Text\nimport com.halilibo.richtext.ui.currentContentColor\nimport com.halilibo.richtext.ui.currentRichTextStyle\nimport com.halilibo.richtext.ui.string.RichTextString.Format\n\n/**\n * Renders a [RichTextString] as created with [richTextString].\n */\n@Suppress(\"UnusedBoxWithConstraintsScope\")\n@Composable\npublic fun RichTextScope.Text(\n  text: RichTextString,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  softWrap: Boolean = true,\n  overflow: TextOverflow = TextOverflow.Clip,\n  maxLines: Int = Int.MAX_VALUE\n) {\n  val style = currentRichTextStyle.stringStyle\n  val contentColor = currentContentColor\n  val annotated = remember(text, style, contentColor) {\n    val resolvedStyle = (style ?: RichTextStringStyle.Default).resolveDefaults()\n    text.toAnnotatedString(resolvedStyle, contentColor)\n  }\n\n  val inlineContents = remember(text) { text.getInlineContents() }\n\n  if (inlineContents.isEmpty()) {\n    Text(\n      text = annotated,\n      onTextLayout = onTextLayout,\n      softWrap = softWrap,\n      overflow = overflow,\n      maxLines = maxLines\n    )\n  } else {\n    // expensive constraints reading path\n    BoxWithConstraints(modifier = modifier) {\n      val inlineTextContents = manageInlineTextContents(\n        inlineContents = inlineContents,\n        textConstraints = constraints\n      )\n\n      Text(\n        text = annotated,\n        onTextLayout = onTextLayout,\n        inlineContent = inlineTextContents,\n        softWrap = softWrap,\n        overflow = overflow,\n        maxLines = maxLines,\n      )\n    }\n  }\n}\n\nprivate fun AnnotatedString.getConsumableAnnotations(textFormatObjects: Map<String, Any>, offset: Int): Sequence<Format.Link> =\n  getStringAnnotations(Format.FormatAnnotationScope, offset, offset)\n    .asSequence()\n    .mapNotNull {\n      Format.findTag(\n        it.item,\n        textFormatObjects\n      ) as? Format.Link\n    }\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/ConditionalTapGestureDetector.kt",
    "content": "@file:OptIn(ExperimentalComposeUiApi::class)\n\npackage com.halilibo.richtext.ui.util\n\nimport androidx.compose.foundation.gestures.GestureCancellationException\nimport androidx.compose.foundation.gestures.PressGestureScope\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.changedToUp\nimport androidx.compose.ui.input.pointer.isOutOfBounds\nimport androidx.compose.ui.platform.ViewConfiguration\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.util.fastAll\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\n\nprivate val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { }\n\n/**\n * If predicate returns true: detects tap, double-tap, and long press gestures and calls [onTap],\n * [onDoubleTap], and [onLongPress], respectively, when detected. [onPress] is called when the press\n * is detected and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease]\n * can be used to detect when pointers have released or the gesture was canceled.\n * The first pointer down and final pointer up are consumed, and in the\n * case of long press, all changes after the long press is detected are consumed.\n *\n * Each function parameter receives an [Offset] representing the position relative to the containing\n * element. The [Offset] can be outside the actual bounds of the element itself meaning the numbers\n * can be negative or larger than the element bounds if the touch target is smaller than the\n * [ViewConfiguration.minimumTouchTargetSize].\n *\n * When [onDoubleTap] is provided, the tap gesture is detected only after\n * the [ViewConfiguration.doubleTapMinTimeMillis] has passed and [onDoubleTap] is called if the\n * second tap is started before [ViewConfiguration.doubleTapTimeoutMillis]. If [onDoubleTap] is not\n * provided, then [onTap] is called when the pointer up has been received.\n *\n * After the initial [onPress], if the pointer moves out of the input area, the position change\n * is consumed, or another gesture consumes the down or up events, the gestures are considered\n * canceled. That means [onDoubleTap], [onLongPress], and [onTap] will not be called after a\n * gesture has been canceled.\n *\n * If the first down event is consumed somewhere else, the entire gesture will be skipped,\n * including [onPress].\n */\npublic suspend fun PointerInputScope.detectTapGesturesIf(\n  predicate: (Offset) -> Boolean = { true },\n  onDoubleTap: ((Offset) -> Unit)? = null,\n  onLongPress: ((Offset) -> Unit)? = null,\n  onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,\n  onTap: ((Offset) -> Unit)? = null\n): Unit = coroutineScope {\n  // special signal to indicate to the sending side that it shouldn't intercept and consume\n  // cancel/up events as we're only require down events\n  val pressScope =\n    PressGestureScopeImpl(this@detectTapGesturesIf)\n\n  awaitEachGesture {\n    val down = awaitFirstDown()\n    if (!predicate(down.position)) {\n      pressScope.reset()\n      return@awaitEachGesture\n    }\n    down.consume()\n    pressScope.reset()\n    if (onPress !== NoPressGesture) launch {\n      pressScope.onPress(down.position)\n    }\n    val longPressTimeout = onLongPress?.let {\n      viewConfiguration.longPressTimeoutMillis\n    } ?: (Long.MAX_VALUE / 2)\n    var upOrCancel: PointerInputChange? = null\n    try {\n      // wait for first tap up or long press\n      upOrCancel = withTimeout(longPressTimeout) {\n        waitForUpOrCancellation()\n      }\n      if (upOrCancel == null) {\n        pressScope.cancel() // tap-up was canceled\n      } else {\n        upOrCancel.consume()\n        pressScope.release()\n      }\n    } catch (_: PointerEventTimeoutCancellationException) {\n      onLongPress?.invoke(down.position)\n      consumeUntilUp()\n      pressScope.release()\n    }\n\n    if (upOrCancel != null) {\n      // tap was successful.\n      if (onDoubleTap == null) {\n        onTap?.invoke(upOrCancel.position) // no need to check for double-tap.\n      } else {\n        // check for second tap\n        val secondDown = awaitSecondDown(upOrCancel)\n\n        if (secondDown == null) {\n          onTap?.invoke(upOrCancel.position) // no valid second tap started\n        } else {\n          // Second tap down detected\n          pressScope.reset()\n          if (onPress !== NoPressGesture) {\n            launch { pressScope.onPress(secondDown.position) }\n          }\n\n          try {\n            // Might have a long second press as the second tap\n            withTimeout(longPressTimeout) {\n              val secondUp = waitForUpOrCancellation()\n              if (secondUp != null) {\n                secondUp.consume()\n                pressScope.release()\n                onDoubleTap(secondUp.position)\n              } else {\n                pressScope.cancel()\n                onTap?.invoke(upOrCancel.position)\n              }\n            }\n          } catch (e: PointerEventTimeoutCancellationException) {\n            // The first tap was valid, but the second tap is a long press.\n            // notify for the first tap\n            onTap?.invoke(upOrCancel.position)\n\n            // notify for the long press\n            onLongPress?.invoke(secondDown.position)\n            consumeUntilUp()\n            pressScope.release()\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * Consumes all pointer events until nothing is pressed and then returns. This method assumes\n * that something is currently pressed.\n */\nprivate suspend fun AwaitPointerEventScope.consumeUntilUp() {\n  do {\n    val event = awaitPointerEvent()\n    event.changes.fastForEach { it.consume() }\n  } while (event.changes.fastAny { it.pressed })\n}\n\n/**\n * Waits for [ViewConfiguration.doubleTapTimeoutMillis] for a second press event. If a\n * second press event is received before the time out, it is returned or `null` is returned\n * if no second press is received.\n */\nprivate suspend fun AwaitPointerEventScope.awaitSecondDown(\n  firstUp: PointerInputChange\n): PointerInputChange? = withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {\n  val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis\n  var change: PointerInputChange\n  // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap\n  do {\n    change = awaitFirstDown()\n  } while (change.uptimeMillis < minUptime)\n  change\n}\n\n/**\n * Reads events until all pointers are up or the gesture was canceled. The gesture\n * is considered canceled when a pointer leaves the event region, a position change\n * has been consumed or a pointer down change event was consumed in the [PointerEventPass.Main]\n * pass. If the gesture was not canceled, the final up change is returned or `null` if the\n * event was canceled.\n */\nprivate suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? {\n  while (true) {\n    val event = awaitPointerEvent(PointerEventPass.Main)\n    if (event.changes.fastAll { it.changedToUp() }) {\n      // All pointers are up\n      return event.changes[0]\n    }\n\n    if (event.changes.fastAny {\n        it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)\n      }\n    ) {\n      return null // Canceled\n    }\n\n    // Check for cancel by position consumption. We can look on the Final pass of the\n    // existing pointer event because it comes after the Main pass we checked above.\n    val consumeCheck = awaitPointerEvent(PointerEventPass.Final)\n    if (consumeCheck.changes.fastAny { it.isConsumed }) {\n      return null\n    }\n  }\n}\n\n/**\n * [detectTapGesturesIf]'s implementation of [PressGestureScope].\n */\nprivate class PressGestureScopeImpl(\n  density: Density\n) : PressGestureScope, Density by density {\n  private var isReleased = false\n  private var isCanceled = false\n  private val mutex = Mutex(locked = false)\n\n  /**\n   * Called when a gesture has been canceled.\n   */\n  fun cancel() {\n    isCanceled = true\n    mutex.unlock()\n  }\n\n  /**\n   * Called when all pointers are up.\n   */\n  fun release() {\n    isReleased = true\n    mutex.unlock()\n  }\n\n  /**\n   * Called when a new gesture has started.\n   */\n  fun reset() {\n    mutex.tryLock() // If tryAwaitRelease wasn't called, this will be unlocked.\n    isReleased = false\n    isCanceled = false\n  }\n\n  override suspend fun awaitRelease() {\n    if (!tryAwaitRelease()) {\n      throw GestureCancellationException(\"The press gesture was canceled.\")\n    }\n  }\n\n  override suspend fun tryAwaitRelease(): Boolean {\n    if (!isReleased && !isCanceled) {\n      mutex.lock()\n    }\n    return isReleased\n  }\n}\n"
  },
  {
    "path": "richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/UUID.kt",
    "content": "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",
    "content": "package com.halilibo.richtext.ui.util\n\nimport java.util.UUID\n\ninternal actual fun randomUUID(): String {\n  return UUID.randomUUID().toString()\n}"
  },
  {
    "path": "richtext-ui/src/jvmMain/kotlin/com/halilibo/richtext/ui/CodeBlock.desktop.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.HorizontalScrollbar\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.rememberScrollbarAdapter\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\n\nprivate val LocalScrollbarEnabled = compositionLocalOf { true }\n\n@Composable\ninternal actual fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n) {\n  if (!wordWrap) {\n    val scrollState = rememberScrollState()\n    Column {\n      children(Modifier.horizontalScroll(scrollState))\n      if (LocalScrollbarEnabled.current) {\n        val horizontalScrollbarAdapter = rememberScrollbarAdapter(scrollState)\n        HorizontalScrollbar(adapter = horizontalScrollbarAdapter)\n      }\n    }\n  } else {\n    children(Modifier)\n  }\n}\n\n/**\n * Contextually disables scrollbar for Desktop CodeBlocks under [content] tree.\n */\n@Composable\npublic fun DisableScrollbar(\n  content: @Composable () -> Unit\n) {\n  CompositionLocalProvider(LocalScrollbarEnabled provides false) {\n    content()\n  }\n}\n"
  },
  {
    "path": "richtext-ui-material/build.gradle.kts",
    "content": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo.richtext.ui.material\"\n  }\n  sourceSets {\n    val commonMain by getting {\n      dependencies {\n        implementation(compose.runtime)\n        implementation(compose.foundation)\n        implementation(compose.material)\n        api(project(\":richtext-ui\"))\n      }\n    }\n    val commonTest by getting\n\n    val androidMain by getting\n    val jvmMain by getting\n  }\n}\n"
  },
  {
    "path": "richtext-ui-material/gradle.properties",
    "content": "POM_NAME=Compose Richtext UI Material\nPOM_DESCRIPTION=An extension library for RichText UI to easily bind with Material apps."
  },
  {
    "path": "richtext-ui-material/src/androidMain/AndroidManifest.xml",
    "content": "<manifest />"
  },
  {
    "path": "richtext-ui-material/src/commonMain/kotlin/com/halilibo/richtext/ui/material/RichText.kt",
    "content": "package com.halilibo.richtext.ui.material\n\nimport androidx.compose.material.LocalContentColor\nimport androidx.compose.material.LocalTextStyle\nimport androidx.compose.material.ProvideTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport com.halilibo.richtext.ui.BasicRichText\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.RichTextThemeProvider\n\n/**\n * RichText implementation that integrates with Material design.\n *\n * If the consumer app has small composition trees or only uses RichText in\n * a single place, it would be ideal to call this function instead of wrapping\n * everything under [RichTextMaterialTheme].\n */\n@Composable\npublic fun RichText(\n  modifier: Modifier = Modifier,\n  style: RichTextStyle? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  RichTextMaterialTheme {\n    BasicRichText(\n      modifier = modifier,\n      style = style,\n      children = children\n    )\n  }\n}\n\n/**\n * Wraps the given [child] with Material Theme integration for [BasicRichText].\n *\n * This function also keeps track of the parent context by using CompositionLocals\n * to not apply Material Theming if it already exists in the current composition.\n */\n@Composable\ninternal fun RichTextMaterialTheme(\n  child: @Composable () -> Unit\n) {\n  val isApplied = LocalMaterialThemingApplied.current\n\n  if (!isApplied) {\n    RichTextThemeProvider(\n      textStyleProvider = { LocalTextStyle.current },\n      contentColorProvider = { LocalContentColor.current },\n      textStyleBackProvider = { textStyle, content ->\n        ProvideTextStyle(textStyle, content)\n      },\n      contentColorBackProvider = { color, content ->\n        CompositionLocalProvider(LocalContentColor provides color) {\n          content()\n        }\n      }\n    ) {\n      CompositionLocalProvider(LocalMaterialThemingApplied provides true) {\n        child()\n      }\n    }\n  } else {\n    child()\n  }\n}\n\nprivate val LocalMaterialThemingApplied = compositionLocalOf { false }\n"
  },
  {
    "path": "richtext-ui-material3/build.gradle.kts",
    "content": "plugins {\n  id(\"richtext-kmp-library\")\n  id(\"org.jetbrains.dokka\")\n}\n\nkotlin {\n  android {\n    namespace = \"com.halilibo.richtext.ui.material3\"\n  }\n  sourceSets {\n    val commonMain by getting {\n      dependencies {\n        implementation(compose.runtime)\n        implementation(compose.foundation)\n        implementation(compose.material3)\n\n        api(project(\":richtext-ui\"))\n      }\n    }\n    val commonTest by getting\n\n    val androidMain by getting\n    val jvmMain by getting\n  }\n}\n"
  },
  {
    "path": "richtext-ui-material3/gradle.properties",
    "content": "POM_NAME=Compose Richtext UI Material3\nPOM_DESCRIPTION=An extension library for RichText UI to easily bind with Material3 apps."
  },
  {
    "path": "richtext-ui-material3/src/androidMain/AndroidManifest.xml",
    "content": "<manifest />"
  },
  {
    "path": "richtext-ui-material3/src/commonMain/kotlin/com/halilibo/richtext/ui/material3/RichText.kt",
    "content": "package com.halilibo.richtext.ui.material3\n\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport com.halilibo.richtext.ui.BasicRichText\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.RichTextThemeProvider\n\n/**\n * RichText implementation that integrates with Material 3 design.\n *\n * If the consumer app has small composition trees or only uses RichText in\n * a single place, it would be ideal to call this function instead of wrapping\n * everything under [RichTextMaterialTheme].\n */\n@Composable\npublic fun RichText(\n  modifier: Modifier = Modifier,\n  style: RichTextStyle? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  RichTextMaterialTheme {\n    BasicRichText(\n      modifier = modifier,\n      style = style,\n      children = children\n    )\n  }\n}\n\n/**\n * Wraps the given [child] with Material Theme integration for [BasicRichText].\n *\n * This function also keeps track of the parent context by using CompositionLocals\n * to not apply Material Theming if it already exists in the current composition.\n */\n@Composable\ninternal fun RichTextMaterialTheme(\n  child: @Composable () -> Unit\n) {\n  val isApplied = LocalMaterialThemingApplied.current\n\n  if (!isApplied) {\n    RichTextThemeProvider(\n      textStyleProvider = { LocalTextStyle.current },\n      contentColorProvider = { LocalContentColor.current },\n      textStyleBackProvider = { textStyle, content ->\n        ProvideTextStyle(textStyle, content)\n      },\n      contentColorBackProvider = { color, content ->\n        CompositionLocalProvider(LocalContentColor provides color) {\n          content()\n        }\n      }\n    ) {\n      CompositionLocalProvider(LocalMaterialThemingApplied provides true) {\n        child()\n      }\n    }\n  } else {\n    child()\n  }\n}\n\nprivate val LocalMaterialThemingApplied = compositionLocalOf { false }"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n  repositories {\n    google()\n    gradlePluginPortal()\n    mavenCentral()\n  }\n}\n\ninclude(\":richtext-ui\")\ninclude(\":richtext-ui-material\")\ninclude(\":richtext-ui-material3\")\ninclude(\":richtext-commonmark\")\ninclude(\":richtext-markdown\")\ninclude(\":android-sample\")\ninclude(\":desktop-sample\")\nrootProject.name = \"compose-richtext\"\n"
  }
]