Skip to content

Commit

Permalink
Merge pull request #92 from zeplin/feature/wildcard-support-on-config
Browse files Browse the repository at this point in the history
Support wildcard for zeplin component on config
  • Loading branch information
fonurr authored Jun 8, 2020
2 parents 7b3be0d + 7f8a228 commit 658ac14
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 51 deletions.
96 changes: 73 additions & 23 deletions src/coco/zeplinComponent/data/ZeplinComponentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,100 @@ import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/d
import { getConfig } from "../../config/util/configUtil";
import BarrelType from "../../../common/domain/barrel/BarrelType";
import BarrelDetails from "../../../common/domain/zeplinComponent/model/BarrelDetails";
import { promiseAny } from "../../../common/general/promiseUtil";
import { flatten } from "../../../common/general/arrayUtil";
import { replaceAll } from "../../../common/general/stringUtil";

const WILDCARD = "*";

type ZeplinComponentData = { component: ZeplinComponent; providerId: string; providerType: BarrelType };

export default class ZeplinComponentStore implements Store<ZeplinComponentData, BaseError> {
public constructor(private name: string, private configPath: string) { }
export default class ZeplinComponentStore implements Store<ZeplinComponentData[], BaseError> {
private readonly matcher: RegExp;

public constructor(private name: string, private configPath: string) {
this.matcher = this.getMatcher(name);
}

private getMatcher(name: string): RegExp {
const escapedName = name.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); // Escape special chars for regex except "*"
const wilcardReadyName = `^${escapedName.replace(/\*/g, ".*")}$`; // Replace "*" to match any number of chars
return new RegExp(wilcardReadyName);
}

private getMatcherLength(matcher: string): number {
return replaceAll(matcher, WILDCARD, "").length;
}

private filterMoreSpecificMatchers(names: string[]): RegExp[] {
if (!this.name.includes(WILDCARD)) {
return [];
}

let toCompareReached = false;
const moreSpecificNames = [];
const toCompareLength = this.getMatcherLength(this.name);

for (const name of names) {
if (name === this.name) {
toCompareReached = true;
} else if (name.includes(WILDCARD)) {
const nameLength = this.getMatcherLength(name);
if (nameLength > toCompareLength || (nameLength === toCompareLength && !toCompareReached)) {
moreSpecificNames.push(name);
}
} else if (this.matcher.test(name)) {
moreSpecificNames.push(name);
}
}

public get = (): Promise<Result<ZeplinComponentData, BaseError>> => {
const barrels = getConfig(this.configPath).getBarrelsWithTypes();
return moreSpecificNames.map(this.getMatcher);
}

return promiseAny(barrels.map(async ({ id, type }): Promise<ZeplinComponentData> => {
public get = async (): Promise<Result<ZeplinComponentData[], BaseError>> => {
const config = getConfig(this.configPath);
const barrels = config.getBarrelsWithTypes();
const configComponentNames = config.getAllZeplinComponentNames();
const moreSpecificMatchers = this.filterMoreSpecificMatchers(configComponentNames);

const allSearches = Promise.all(barrels.map(async ({ id, type }): Promise<ZeplinComponentData[]> => {
const leafId = id;
const leafType = type;
let currentId: string | undefined = leafId;
let currentType = leafType;
const componentDatas: ZeplinComponentData[] = [];

while (currentId) {
const childId: string | undefined = currentId === leafId ? undefined : leafId;
const childType: BarrelType | undefined = currentId === leafId ? undefined : leafType;

const { data }: Result<BarrelDetails, BaseError> =
await BarrelDetailsStoreProvider.get(currentId, currentType, childId, childType).get();
const component = data?.components.find(current => current.name === this.name);
if (component) {
return {
component,
providerId: leafId,
providerType: leafType
} as ZeplinComponentData;
} else {
currentId = data?.parentId;
currentType = BarrelType.Styleguide;
}

const components = data?.components.filter(current =>
!moreSpecificMatchers.some(matcher => matcher.test(current.name)) && this.matcher.test(current.name)
) ?? [];
componentDatas.push(...components.map(component => ({
component,
providerId: leafId,
providerType: leafType
} as ZeplinComponentData)));

currentId = data?.parentId;
currentType = BarrelType.Styleguide;
}

throw (new Error(`Component not found in barrel ${id}`));
})).then(data => ({
data
} as Result<ZeplinComponentData, BaseError>)).catch(() => ({
errors: [new BaseError()]
return componentDatas;
}));

const allMatches = flatten(await allSearches);
const exactMatches = allMatches.filter(data => data.component.name === this.name);
return {
data: exactMatches.length ? exactMatches : allMatches,
errors: allMatches.length ? undefined : [new BaseError()]
};
};

public refresh = (): Promise<Result<ZeplinComponentData, BaseError>> => {
public refresh = (): Promise<Result<ZeplinComponentData[], BaseError>> => {
BarrelDetailsStoreProvider.clearCache();
return this.get();
};
Expand Down
67 changes: 40 additions & 27 deletions src/coco/zeplinComponent/hover/ZeplinComponentHoverCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ZeplinComponentStore from "../data/ZeplinComponentStore";
import localization from "../../../localization";
import { getConfig } from "../../config/util/configUtil";
import { getZeplinComponentDetailRepresentation } from "../../../common/domain/zeplinComponent/util/zeplinComponentUi";
import { boldenForMarkdown, getMarkdownCommand } from "../../../common/vscode/hover/hoverUtil";
import { boldenForMarkdown, getMarkdownCommand, escapeMarkdownChars } from "../../../common/vscode/hover/hoverUtil";
import { getComponentAppUri, getComponentWebUrl } from "../../../common/domain/openInZeplin/util/zeplinUris";
import { getOpenInZeplinLinks, getMarkdownRefreshIcon } from "../../../common/domain/hover/zeplinHoverUtil";
import { getImageSize } from "../../../common/general/imageUtil";
Expand All @@ -22,36 +22,49 @@ class ZeplinComponentHoverCreator implements ConfigHoverCreator {
public async create(configPath: string, word: string): Promise<vscode.Hover> {
const { data, errors } = await new ZeplinComponentStore(word, configPath).get();

if (errors && errors.length) {
return new HoverBuilder().append(localization.coco.zeplinComponent.notFound(word)).build();
if (errors?.length) {
return new HoverBuilder()
.append(localization.coco.zeplinComponent.notFound(escapeMarkdownChars(word)))
.build();
} else {
const { component, providerId, providerType } = data!;
const thumbnailUrl = component.latestVersion.snapshot.url;
const thumbnailSize = await getImageSize(thumbnailUrl);
const description = component.description && component.description.length > MAX_DESCRIPTION_LENGTH
? `${component.description.substring(0, MAX_DESCRIPTION_LENGTH)}…`
: component.description;
const appUri = getComponentAppUri(providerId, providerType, component._id);
const webUrl = getComponentWebUrl(providerId, providerType, component._id);
const builder = new HoverBuilder();
const thumbnailSizes =
await Promise.all(data!.map(({ component }) => getImageSize(component.latestVersion.snapshot.url)));
for (let componentIndex = 0; componentIndex < data!.length; componentIndex++) {
const { component, providerId, providerType } = data![componentIndex];
const thumbnailUrl = component.latestVersion.snapshot.url;
const thumbnailSize = thumbnailSizes[componentIndex];
const description = component.description && component.description.length > MAX_DESCRIPTION_LENGTH
? `${component.description.substring(0, MAX_DESCRIPTION_LENGTH)}…`
: component.description;
const appUri = getComponentAppUri(providerId, providerType, component._id);
const webUrl = getComponentWebUrl(providerId, providerType, component._id);

const builder = new HoverBuilder()
.append(
`${boldenForMarkdown(component.name)} ` +
`${getMarkdownCommand(ClearCacheCommand.name, getMarkdownRefreshIcon())}`
)
.appendLine(true)
.appendImage(thumbnailUrl, thumbnailSize.width, thumbnailSize.height, undefined, MAX_THUMBNAIL_HEIGHT)
.appendLine();
if (description) {
if (componentIndex !== 0) {
builder.appendLine().appendHorizontalLine().appendLine();
}
builder
.append(description)
.appendLine(true);
.append(
`${boldenForMarkdown(component.name)} ` +
`${getMarkdownCommand(ClearCacheCommand.name, getMarkdownRefreshIcon())}`
)
.appendLine(true)
.appendImage(
thumbnailUrl, thumbnailSize.width, thumbnailSize.height, undefined, MAX_THUMBNAIL_HEIGHT
)
.appendLine();
if (description) {
builder
.append(description)
.appendLine(true);
}
builder.append(getZeplinComponentDetailRepresentation(component))
.appendLine()
.appendLine()
.append(getOpenInZeplinLinks(appUri, webUrl));
}
return builder.append(getZeplinComponentDetailRepresentation(component))
.appendLine()
.appendLine()
.append(getOpenInZeplinLinks(appUri, webUrl))
.build();

return builder.build();
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/common/vscode/hover/hoverUtil.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
const MARKDOWN_SPACE = "&nbsp;";
const MARKDOWN_SPECIAL_CHARS_REG_EXP = /[.*#+-?^${}()!|[\]\\`]/g;
const MAX_IMAGE_HEIGHT = 480;
const MAX_IMAGE_WIDTH = 480;

/**
* Return text with escaped chars for markdown.
* @param text Text to be escaped
*/
function escapeMarkdownChars(text: string): string {
return text.replace(MARKDOWN_SPECIAL_CHARS_REG_EXP, "\\$&");
}

/**
* Returns markdown for an image uri. Restricts width and height wrt maxWidth and maxHeight if provided.
* @param uri Uri of an image.
Expand Down Expand Up @@ -88,6 +97,7 @@ function toItalicForMarkdown(text: string): string {

export {
MARKDOWN_SPACE,
escapeMarkdownChars,
getMarkdownImage,
getMarkdownLink,
getMarkdownCommand,
Expand Down
2 changes: 1 addition & 1 deletion src/localization/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const localization = {
join: "Join",
connected: (count: number) => `Connected to ${pluralize(count, "Zeplin Component")}.`,
connect: "Connect to Zeplin components",
notFound: (name: string) => `No component named${name}” in projects/styleguides you’re a member of`
notFound: (name: string) => `No component matching${name}” in projects/styleguides you’re a member of`
},
common: {
noConfigFound: "Please create a Zeplin configuration file first.",
Expand Down

0 comments on commit 658ac14

Please sign in to comment.