diff --git a/ec.config.mjs b/ec.config.mjs
index 7b0075176..573e0a392 100644
--- a/ec.config.mjs
+++ b/ec.config.mjs
@@ -1,12 +1,14 @@
-import { defineEcConfig } from 'astro-expressive-code'
+import { defineEcConfig } from 'astro-expressive-code';
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
+import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
export default defineEcConfig({
defaultProps: {
- wrap: true
+ wrap: true,
+ showLineNumbers: true,
},
// This is where you can pass your plugin options
- plugins: [pluginCollapsibleSections()],
+ plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
frames: {
extractFileNameFromCode: true
},
diff --git a/markdoc.config.mjs b/markdoc.config.mjs
index d5fc53a86..89acd1316 100644
--- a/markdoc.config.mjs
+++ b/markdoc.config.mjs
@@ -77,6 +77,15 @@ export default defineMarkdocConfig({
type: Boolean,
required: false,
default: false
+ },
+ showLineNumbers: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ startLineNumber: {
+ type: Number,
+ required: false,
}
}
},
@@ -133,5 +142,19 @@ export default defineMarkdocConfig({
tabs: {
render: component("src/components/Tabs.astro"),
},
+ exampleapp: {
+ render: component("src/components/ExampleApp.astro"),
+ attributes: {
+ permalink: {
+ type: String,
+ required: true,
+ },
+ lang: {
+ type: String,
+ required: false,
+ default: "txt",
+ }
+ }
+ }
}
})
diff --git a/package-lock.json b/package-lock.json
index e108c6d2a..3bebd39aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.2",
"@expressive-code/plugin-collapsible-sections": "^0.35.6",
+ "@expressive-code/plugin-line-numbers": "^0.38.3",
"@nanostores/persistent": "^0.10.2",
"@nanostores/react": "^0.8.0",
"@octokit/core": "^6.1.2",
@@ -1586,6 +1587,32 @@
"@expressive-code/core": "^0.35.6"
}
},
+ "node_modules/@expressive-code/plugin-line-numbers": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-line-numbers/-/plugin-line-numbers-0.38.3.tgz",
+ "integrity": "sha512-QbK9NL44ST9w5ANVEu0a7fkjlq+fXgxyPqiSyFC4Nw/sAXd0MUwT1C8V0qlve4pZYLz53CR9tn4JQQbR0Z1tOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@expressive-code/core": "^0.38.3"
+ }
+ },
+ "node_modules/@expressive-code/plugin-line-numbers/node_modules/@expressive-code/core": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.38.3.tgz",
+ "integrity": "sha512-s0/OtdRpBONwcn23O8nVwDNQqpBGKscysejkeBkwlIeHRLZWgiTVrusT5Idrdz1d8cW5wRk9iGsAIQmwDPXgJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.0.4",
+ "hast-util-select": "^6.0.2",
+ "hast-util-to-html": "^9.0.1",
+ "hast-util-to-text": "^4.0.1",
+ "hastscript": "^9.0.0",
+ "postcss": "^8.4.38",
+ "postcss-nested": "^6.0.1",
+ "unist-util-visit": "^5.0.0",
+ "unist-util-visit-parents": "^6.0.1"
+ }
+ },
"node_modules/@expressive-code/plugin-shiki": {
"version": "0.35.6",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.35.6.tgz",
diff --git a/package.json b/package.json
index 94aee7d96..cdfec5a36 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.2",
"@expressive-code/plugin-collapsible-sections": "^0.35.6",
+ "@expressive-code/plugin-line-numbers": "^0.38.3",
"@nanostores/persistent": "^0.10.2",
"@nanostores/react": "^0.8.0",
"@octokit/core": "^6.1.2",
diff --git a/src/components/CodeBlock.astro b/src/components/CodeBlock.astro
index e4c48c3ce..0670137aa 100644
--- a/src/components/CodeBlock.astro
+++ b/src/components/CodeBlock.astro
@@ -10,6 +10,8 @@ interface Props {
ins?: string;
del?: string;
useDiffSyntax?: boolean;
+ showLineNumbers?: boolean;
+ startLineNumber: Number;
}
const {
@@ -19,64 +21,47 @@ const {
del,
collapse,
useDiffSyntax = false,
+ showLineNumbers = false,
+ startLineNumber = undefined,
} = Astro.props as Props;
-let content = "";
-
-if (Astro.slots.has("default")) {
- // Get the raw content of the slot without transformations
- content = await Astro.slots.render("default");
-}
-
-const parseHighlight = (
- highlight: string,
-): MarkerDefinition | MarkerDefinition[] => {
+const parseHighlight = (highlight: string): MarkerDefinition[] => {
return highlight.split(",").map((item) => {
const trimmedItem = item.trim();
-
- if (trimmedItem.startsWith("{") && trimmedItem.endsWith("}")) {
+ if (/^\{.*\}$/.test(trimmedItem)) {
const innerContent = trimmedItem.slice(1, -1).trim();
-
- // Check if the inner content contains a colon
if (innerContent.includes(":")) {
const [key, value] = innerContent.split(":").map((part) => part.trim());
- return { [key]: value.replace(/"/g, "") } as MarkerDefinition;
+ return { [key]: value.replace(/['"]/g, "") } as MarkerDefinition;
}
-
- // If no colon, treat it as a regular string token
- return trimmedItem;
- }
-
- if (trimmedItem.startsWith("/") && trimmedItem.endsWith("/")) {
+ } else if (/^\/.*\/$/.test(trimmedItem)) {
return new RegExp(trimmedItem.slice(1, -1), "g") as MarkerDefinition;
}
-
- return trimmedItem.replace(/'/g, "");
+ return trimmedItem.replace(/['"]/g, "") as MarkerDefinition;
}) as MarkerDefinition[];
};
-const parseCollapse = (collapse?: string | string[]): string[] => {
- if (typeof collapse === "string") {
- return collapse.split(",").map((item) => item.trim());
- }
+const parseCollapse = (collapse?: string | string[]): string[] =>
+ typeof collapse === "string"
+ ? collapse.split(",").map((item) => item.trim())
+ : collapse || [];
- return Array.isArray(collapse) ? collapse : [];
-};
+const { code, lang } = await extractCodeFromHTML(
+ await Astro.slots.render("default"),
+);
-const { code, lang } = await extractCodeFromHTML(content);
-const parsedHighlight = highlight ? parseHighlight(highlight) : undefined;
-const parsedIns = ins ? parseHighlight(ins) : undefined;
-const parsedDel = del ? parseHighlight(del) : undefined;
-const parsedCollapse = collapse ? parseCollapse(collapse) : undefined;
+const codeAttributes = {
+ title,
+ lang,
+ code,
+ collapse: collapse ? parseCollapse(collapse) : [],
+ mark: highlight ? parseHighlight(highlight) : [],
+ ins: ins ? parseHighlight(ins) : [],
+ del: del ? parseHighlight(del) : [],
+ useDiffSyntax,
+ showLineNumbers,
+ ...(startLineNumber && { startLineNumber }),
+};
---
-
+
diff --git a/src/components/DefList.astro b/src/components/DefList.astro
index e9625e109..2d41290ea 100644
--- a/src/components/DefList.astro
+++ b/src/components/DefList.astro
@@ -1,7 +1,6 @@
---
-import { decode } from "tiny-decode";
import { parseDefList } from "@components/utils/parseDefList";
-const content = decode(await Astro.slots.render("default"));
+const content = await Astro.slots.render("default");
const parsedContent = await parseDefList(content);
---
diff --git a/src/components/ExampleApp.astro b/src/components/ExampleApp.astro
new file mode 100644
index 000000000..d18f2dc6c
--- /dev/null
+++ b/src/components/ExampleApp.astro
@@ -0,0 +1,19 @@
+---
+import { fetchExampleApp } from "@components/utils/fetchExampleApp";
+import { Code as ExpressiveCode } from "astro-expressive-code/components";
+
+interface Props {
+ permalink: string;
+ lang: string;
+}
+
+const { permalink, lang } = Astro.props as Props;
+const { code, title } = await fetchExampleApp(permalink);
+const codeAttributes = {
+ code,
+ lang,
+ title,
+};
+---
+
+
diff --git a/src/components/utils/extractCode.ts b/src/components/utils/extractCode.ts
index 8b6452dba..6ed71a0eb 100644
--- a/src/components/utils/extractCode.ts
+++ b/src/components/utils/extractCode.ts
@@ -1,6 +1,6 @@
import { unified } from "unified";
import rehypeParse from "rehype-parse";
-import { selectAll, select } from "hast-util-select";
+import { select, selectAll } from "hast-util-select";
import { toString } from "hast-util-to-string";
interface CodeExtractionResult {
@@ -8,12 +8,16 @@ interface CodeExtractionResult {
lang?: string;
}
-export const extractCodeFromHTML = async (htmlString: string): Promise => {
+export const extractCodeFromHTML = async (
+ htmlString: string,
+): Promise => {
const processor = unified().use(rehypeParse, { fragment: true });
const ast = processor.parse(htmlString);
const preElement = select("pre[data-language]", ast);
- const lang = preElement ? preElement.properties["dataLanguage"] as string : "";
+ const lang = preElement
+ ? preElement.properties["dataLanguage"] as string
+ : "";
const codeBlocks = selectAll(".ec-line .code", ast);
diff --git a/src/components/utils/fetchExampleApp.ts b/src/components/utils/fetchExampleApp.ts
new file mode 100644
index 000000000..2ea80205d
--- /dev/null
+++ b/src/components/utils/fetchExampleApp.ts
@@ -0,0 +1,67 @@
+import { Octokit } from "@octokit/core";
+
+const octokit = new Octokit({ auth: import.meta.env.VITE_GITHUB_TOKEN });
+
+/**
+ * Parses a file URL and returns its component parts
+ * @param fileUrl
+ * @returns The component parts of the URL for use in a GraphQL query
+ */
+function parsePermalink(fileUrl: string) {
+ const regex =
+ /https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)/;
+ const match = fileUrl.match(regex);
+ if (!match) {
+ throw new Error("Invalid file URL format.");
+ }
+
+ const [, owner, repo, branch, filePath] = match;
+
+ const filename = filePath.split("/").pop()!;
+
+ return { owner, repo, branch, filePath, filename };
+}
+
+/**
+ * Fetches the text content of a file from GitHub
+ * @param fileUrl
+ * @returns The file name and contents
+ */
+export async function fetchExampleApp(
+ fileUrl: string,
+): Promise<{ code: string; title: string }> {
+ const { owner, repo, branch, filePath, filename } = parsePermalink(fileUrl);
+
+ const query = `
+ query($owner: String!, $repo: String!, $expression: String!) {
+ repository(owner: $owner, name: $repo) {
+ object(expression: $expression) {
+ ... on Blob {
+ text
+ }
+ }
+ }
+ }
+ `;
+
+ const expression = `${branch}:${filePath}`;
+
+ const variables = { owner, repo, expression };
+
+ try {
+ const response = await octokit.graphql<
+ { repository: { object: { text: string } } }
+ >(query, variables);
+
+ if (!response.repository.object) {
+ throw new Error("File not found in the repository.");
+ }
+
+ const { text: code } = response.repository.object;
+ return { code, title: filename };
+ } catch (error) {
+ throw new Error(
+ `Failed to fetch file content: ${(error as Error).message}`,
+ );
+ }
+}
diff --git a/src/content/docs/en/sdk/adobe-extension/android/attribution.mdoc b/src/content/docs/en/sdk/adobe-extension/android/attribution.mdoc
new file mode 100644
index 000000000..7eab55016
--- /dev/null
+++ b/src/content/docs/en/sdk/adobe-extension/android/attribution.mdoc
@@ -0,0 +1,133 @@
+---
+title: Set up an attribution callback
+description: Set up an attribution callback to respond to attribution changes.
+sidebar-position: 2
+---
+
+When Adjust receives install data from the Adjust Android Extension for Adobe Experience SDK, the device is attributed to the source of the install. This attribution information can change if the user is retargeted or interacts with another campaign.
+
+You can configure a callback function to respond to attribution changes. When Adjust receives new attribution information, it sends the data asynchronously back to the device. The callback function receives the device's attribution data as an argument.
+
+Read Adjust's [attribution data policies](https://github.com/adjust/sdks/blob/master/doc/attribution-data.md) for more information about attribution data.
+
+## Reference {% #reference %}
+
+To set a callback function to listen for attribution changes, call the `setOnAttributionChangedListener` method of your `AdjustAdobeExtensionConfig` instance with the following argument:
+
+{% deflist %}
+`onAttributionChangedListener`: `OnAttributionChangedListener`
+
+: A function that returns `void` and receives device attribution information as a serialized JSON object.
+{% /deflist %}
+
+## Tutorial: Create an attribution callback {% #tutorial %}
+
+To configure an attribution callback, you need to create a function and assign it to your `AdjustAdobeExtensionConfig` instance. In this tutorial, you'll build on `MainApp.java` from the [integration guide](/en/sdk/adobe-extension/android/integration) and add an `onAttributionChanged` callback function that outputs the user's attribution information to logs as a string. The final result looks like this:
+
+```java
+import android.app.Application;
+import android.util.Log;
+
+import com.adjust.adobeextension.AdjustAdobeExtension;
+import com.adjust.adobeextension.AdjustAdobeExtensionConfig;
+import com.adobe.marketing.mobile.AdobeCallback;
+import com.adobe.marketing.mobile.Extension;
+import com.adobe.marketing.mobile.Analytics;
+import com.adobe.marketing.mobile.Identity;
+import com.adobe.marketing.mobile.LoggingMode;
+import com.adobe.marketing.mobile.MobileCore;
+
+public class MainApp extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ MobileCore.setApplication(this);
+ MobileCore.setLogLevel(LoggingMode.VERBOSE);
+
+ try {
+ MobileCore.configureWithAppID("your_adobe_app_id");
+
+ AdjustAdobeExtensionConfig config =
+ new AdjustAdobeExtensionConfig(AdjustAdobeExtensionConfig.ENVIRONMENT_SANDBOX);
+ config.setOnAttributionChangedListener(new OnAttributionChangedListener() {
+ @Override
+ public void onAttributionChanged(AdjustAttribution adjustAttribution) {
+ Log.d("example", "Attribution information updated");
+ Log.d("example", "Attribution: " + attribution.toString());
+ }
+ });
+ AdjustAdobeExtension.setConfiguration(config);
+ } catch (Exception e) {
+ Log.e("example", "Exception occurred during configuration: " + e.getMessage());
+ }
+
+ try {
+ List> extensions = Arrays.asList(
+ Analytics.EXTENSION,
+ Identity.EXTENSION,
+ AdjustAdobeExtension.EXTENSION);
+ MobileCore.registerExtensions(extensions, new AdobeCallback