Merge branch 'main' into pr/flowrolltide/80-2

This commit is contained in:
Sina Atalay 2024-05-31 19:31:34 +03:00
commit e119f23acb
33 changed files with 1081 additions and 959 deletions

View File

@ -89,3 +89,22 @@ In some of the tests:
When the `testdata` folder needs to be updated, it can be manually regenerated by setting `update_testdata` to `True` in `conftest.py` and running the tests.
Whenever the `testdata` folder is generated, the files should be reviewed manually to ensure everything works as expected.
## Frequently Asked Questions (FAQ)
### How can I add a new social network to RenderCV?
To add a new social network to RenderCV, go to the `rendercv/data_models.py` file and follow these steps:
1. Append the social network name (for example, "Facebook") to the `SocialNetworkName` type.
2. If necessary, implement its username validation in the `SocialNetwork.check_username` method.
3. Implement its URL generation using the `SocialNetwork.url` method. If the URL can be generated by appending the username to a hostname, only update `url_dictionary`.
4. Finally, include the $\LaTeX$ icon of the social network to the `icon_dictionary` in the `CurriculumVitae.connections` method. RenderCV uses the [`fontawesome5`](https://ctan.org/pkg/fontawesome5?lang=en) package. The available icons can be seen [here](https://fosszone.csd.auth.gr/CTAN/fonts/fontawesome5/doc/fontawesome5.pdf).
Then, the tests should be implemented for the new social network with the following steps:
1. Go to `tests/test_data_models.py` and update `test_social_network_url` accordingly.
2. Go to `tests/conftest.py` and add the new social network to `rendercv_filled_curriculum_vitae_data_model`.
3. Set `update_testdata` to `True` in `conftest.py` and run the tests to update the `testdata` folder.
4. Review the updated `testdata` folder manually to ensure everything works as expected. Then, set `update_testdata` to `False` and push the changes.

View File

@ -17,9 +17,9 @@ locale_catalog:
...
```
- The `cv` section is mandatory. It contains the **content of the CV**.
- The `design` section is optional. It contains the **design options of the CV**. If you don't provide a `design` section, RenderCV will use the default design options with the `classic` theme.
- The `locale_catalog` section is optional. You can provide translations for some of the strings used in the CV, for example, month abbreviations. RenderCV will use English strings if you don't provide a `locale_catalog` section.
- The `cv` field is mandatory. It contains the **content of the CV**.
- The `design` field is optional. It contains the **design options of the CV**. If you don't provide a `design` field, RenderCV will use the default design options with the `classic` theme.
- The `locale_catalog` field is optional. You can provide translations for some of the strings used in the CV, for example, month abbreviations. RenderCV will use English strings if you don't provide a `locale_catalog` field.
!!! tip
To maximize your productivity while editing the input YAML file, set up RenderCV's JSON Schema in your IDE. It will validate your inputs on the fly and give auto-complete suggestions.
@ -40,9 +40,9 @@ locale_catalog:
```
3. Press `Ctrl + Space` to see the auto-complete suggestions.
## "`cv`" section of the YAML input
## "`cv`" field
The `cv` section of the YAML input starts with generic information, as shown below.
The `cv` field of the YAML input starts with generic information, as shown below.
```yaml
cv:
@ -60,7 +60,7 @@ cv:
...
```
1. The available social networks are: {{available_social_networks}}. You can add more social networks by following the same pattern. The social network icons are automatically added to the header of the CV.
1. The available social networks are: {{available_social_networks}}.
None of the values above are required. You can omit any or all of them, and RenderCV will adapt to your input. These generic fields are used in the header of the CV.
@ -84,7 +84,9 @@ cv:
...
```
The `sections` field is a dictionary where the keys are the section titles, and the values are lists. Each item of the list is an entry for that section.
### "`cv.sections`" field
The `cv.sections` field is a dictionary where the keys are the section titles, and the values are lists. Each item of the list is an entry for that section.
Here is an example:
@ -121,7 +123,7 @@ The available entry types are: [`EducationEntry`](#education-entry), [`Experienc
Each entry type is a different object (a dictionary). Below, you can find all the entry types along with their optional/mandatory fields and how they appear in each built-in theme.
{% for entry_name, entry in showcase_entries.items() %}
### {{ entry_name }}
#### {{ entry_name }}
{% if entry_name == "Education Entry" %}
@ -166,8 +168,10 @@ Each entry type is a different object (a dictionary). Below, you can find all th
- `doi`: The DOI of the publication.
- `journal`: The journal of the publication.
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format.
{% elif entry_name == "Normal Entry" %}
**Mandatory Fields:**
- `name`: The name of the entry.
@ -179,6 +183,7 @@ Each entry type is a different object (a dictionary). Below, you can find all th
- `end_date`: The end date in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format or "present".
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format. This will override `start_date` and `end_date`.
- `highlights`: A list of bullet points.
{% elif entry_name == "OneLineEntry" %}
**Mandatory Fields:**
@ -204,14 +209,14 @@ Each entry type is a different object (a dictionary). Below, you can find all th
{{ entry["yaml"] }}
```
{% for figure in entry["figures"] %}
`{{ figure["theme"] }}` theme:
![figure["alt_text"]]({{ figure["path"] }})
=== "`{{ figure["theme"] }}` theme"
![figure["alt_text"]]({{ figure["path"] }})
{% endfor %}
{% endfor %}
## "`design`" section of the YAML input
## "`design`" field
The `cv` part of the input contains your content, and the `design` part contains your design options. The `design` part starts with a theme name. Currently, the available themes are: {{available_themes}}. However, custom themes can also be used (see [here](index.md#creating-custom-themes-with-the-create-theme-command).)
The `cv` field of the input contains your content, and the `design` field contains your design options. The `design` field starts with a theme name. Currently, the available themes are: {{available_themes}}. However, custom themes can also be used (see [here](index.md#creating-custom-themes-with-the-create-theme-command).)
```yaml
design:
@ -221,7 +226,7 @@ design:
Each theme may have different options for design. `classic`, `sb2nov`, and `engineeringresumes` almost use identical options, but `moderncv` is slightly different. Please use an IDE that supports JSON schema to avoid missing any available options for the theme (see [above](#structure-of-the-yaml-input-file)).
An example `design` part for a `classic` theme is shown below:
An example `design` field for a `classic` theme is shown below:
```yaml
design:
@ -263,9 +268,9 @@ design:
vertical_between_name_and_connections: 0.3 cm
```
## "`locale_catalog`" section of the YAML input
## "`locale_catalog`" field
This section is what makes RenderCV a multilingual tool. RenderCV uses some English strings to render PDFs. For example, it takes the dates in ISO format (`2020-01-01`) and converts them into human-friendly strings (`"Jan. 2020"`). However, you can override these strings for your own language or needs with the `locale_catalog` section.
This field is what makes RenderCV a multilingual tool. RenderCV uses some English strings to render PDFs. For example, it takes the dates in ISO format (`2020-01-01`) and converts them into human-friendly strings (`"Jan. 2020"`). However, you can override these strings for your own language or needs with the `locale_catalog` field.
Here is an example:

View File

@ -6,6 +6,7 @@ output.
"""
import json
import urllib.request
import pathlib
from typing import Annotated, Callable, Optional
import re
@ -37,11 +38,62 @@ app = typer.Typer(
rich_markup_mode="rich",
add_completion=False,
invoke_without_command=True, # to make rendercv --version work
no_args_is_help=True,
context_settings={"help_option_names": ["-h", "--help"]},
)
def get_latest_version_number_from_pypi() -> Optional[str]:
"""Get the latest version number of RenderCV from PyPI.
Example:
```python
get_latest_version_number_from_pypi()
```
will return:
`#!python "1.1"`
Returns:
Optional[str]: The latest version number of RenderCV from PyPI. Returns None if
the version number cannot be fetched.
"""
version = None
url = "https://pypi.org/pypi/rendercv/json"
try:
with urllib.request.urlopen(url) as response:
data = response.read()
encoding = response.info().get_content_charset("utf-8")
json_data = json.loads(data.decode(encoding))
version = json_data["info"]["version"]
except Exception:
pass
return version
def warn_if_new_version_is_available() -> bool:
"""Check if a new version of RenderCV is available and print a warning message if
there is a new version. Also, return True if there is a new version, and False
otherwise.
Returns:
bool: True if there is a new version, and False otherwise.
"""
latest_version = get_latest_version_number_from_pypi()
if latest_version is not None and __version__ != latest_version:
warning(
f"A new version of RenderCV is available! You are using v{__version__},"
f" and the latest version is v{latest_version}."
)
return True
else:
return False
def welcome():
"""Print a welcome message to the terminal."""
warn_if_new_version_is_available()
table = rich.table.Table(
title=(
"\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
@ -106,7 +158,7 @@ def information(text: str):
Args:
text (str): The text of the information message.
"""
print(f"[bold green]{text}")
print(f"[yellow]{text}")
def get_error_message_and_location_and_value_from_a_custom_error(
@ -326,7 +378,11 @@ def handle_exceptions(function: Callable) -> Callable:
except pydantic.ValidationError as e:
handle_validation_error(e)
except ruamel.yaml.YAMLError as e:
error("There is a YAML error in the input file!", e)
error(
"There is a YAML error in the input file!\n\nTry to use quotation marks"
" to make sure the YAML parser understands the field is a string.",
e,
)
except FileNotFoundError as e:
error(e)
except UnicodeDecodeError as e:
@ -428,7 +484,7 @@ class LiveProgressReporter(rich.live.Live):
"""End the live progress reporting."""
self.overall_progress.update(
self.overall_task_id,
description=f"[bold green]{self.end_message}",
description=f"[yellow]{self.end_message}",
)
@ -521,8 +577,8 @@ def parse_data_model_override_arguments(
@app.command(
name="render",
help=(
"Render a YAML input file. Example: [bold green]rendercv render"
" John_Doe_CV.yaml[/bold green]"
"Render a YAML input file. Example: [yellow]rendercv render"
" John_Doe_CV.yaml[/yellow]. Details: [cyan]rendercv render --help[/cyan]"
),
# allow extra arguments for updating the data model:
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
@ -530,12 +586,13 @@ def parse_data_model_override_arguments(
@handle_exceptions
def cli_command_render(
input_file_name: Annotated[
str,
typer.Argument(help="Name of the YAML input file."),
str, typer.Argument(help="Name of the YAML input file.")
],
use_local_latex_command: Annotated[
Optional[str],
typer.Option(
"--use-local-latex-command",
"-use",
help=(
"Use the local LaTeX installation with the given command instead of the"
" RenderCV's TinyTeX."
@ -545,36 +602,48 @@ def cli_command_render(
output_folder_name: Annotated[
str,
typer.Option(
"--output-folder-name",
"-o",
help="Name of the output folder.",
),
] = "rendercv_output",
latex_path: Annotated[
Optional[str],
typer.Option(
"--latex-path",
"-latex",
help="Copy the LaTeX file to the given path.",
),
] = None,
pdf_path: Annotated[
Optional[str],
typer.Option(
"--pdf-path",
"-pdf",
help="Copy the PDF file to the given path.",
),
] = None,
markdown_path: Annotated[
Optional[str],
typer.Option(
"--markdown-path",
"-md",
help="Copy the Markdown file to the given path.",
),
] = None,
html_path: Annotated[
Optional[str],
typer.Option(
"--html-path",
"-html",
help="Copy the HTML file to the given path.",
),
] = None,
png_path: Annotated[
Optional[str],
typer.Option(
"--png-path",
"-png",
help="Copy the PNG file to the given path.",
),
] = None,
@ -582,6 +651,7 @@ def cli_command_render(
bool,
typer.Option(
"--dont-generate-markdown",
"-nomd",
help="Don't generate the Markdown and HTML file.",
),
] = False,
@ -589,6 +659,7 @@ def cli_command_render(
bool,
typer.Option(
"--dont-generate-html",
"-nohtml",
help="Don't generate the HTML file.",
),
] = False,
@ -596,6 +667,7 @@ def cli_command_render(
bool,
typer.Option(
"--dont-generate-png",
"-nopng",
help="Don't generate the PNG file.",
),
] = False,
@ -711,8 +783,8 @@ def cli_command_render(
@app.command(
name="new",
help=(
"Generate a YAML input file to get started. Example: [bold green]rendercv new"
' "John Doe"[/bold green]'
"Generate a YAML input file to get started. Example: [yellow]rendercv new"
' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]'
),
)
def cli_command_new(
@ -783,8 +855,9 @@ def cli_command_new(
@app.command(
name="create-theme",
help=(
"Create a custom theme folder based on an existing theme. Example: [bold"
" green]rendercv create-theme --based-on classic customtheme[/bold green]"
"Create a custom theme folder based on an existing theme. Example:"
" [yellow]rendercv create-theme customtheme[/yellow]. Details: [cyan]rendercv"
" create-theme --help[/cyan]"
),
)
def cli_command_create_theme(
@ -844,8 +917,10 @@ def cli_command_create_theme(
@app.callback()
def main(
version_requested: Annotated[
Optional[bool], typer.Option("--version", help="Show the version.")
Optional[bool], typer.Option("--version", "-v", help="Show the version.")
] = None,
):
if version_requested:
information(f"RenderCV v{__version__}")
there_is_a_new_version = warn_if_new_version_is_available()
if not there_is_a_new_version:
print(f"RenderCV v{__version__}")

View File

@ -641,15 +641,15 @@ Entry = (
| BulletEntry
| str
)
ListOfEntries = (
list[OneLineEntry]
| list[NormalEntry]
| list[ExperienceEntry]
| list[EducationEntry]
| list[PublicationEntry]
| list[BulletEntry]
| list[str]
)
ListOfEntries = list[
OneLineEntry
| NormalEntry
| ExperienceEntry
| EducationEntry
| PublicationEntry
| BulletEntry
| str
]
entry_types = Entry.__args__[:-1] # a tuple of all the entry types except str
entry_type_names = [entry_type.__name__ for entry_type in entry_types] + ["TextEntry"]
@ -838,6 +838,7 @@ SocialNetworkName = Literal[
"StackOverflow",
"ResearchGate",
"YouTube",
"Google Scholar",
]
available_social_networks = get_args(SocialNetworkName)
@ -909,6 +910,7 @@ class SocialNetwork(RenderCVBaseModel):
"ResearchGate": "https://researchgate.net/profile/",
"YouTube": "https://youtube.com/",
"Google Scholar": "https://scholar.google.com/citations?user=",
"Google Scholar": "https://scholar.google.com/citations?user=",
}
url = url_dictionary[self.network] + self.username
@ -1019,6 +1021,7 @@ class CurriculumVitae(RenderCVBaseModel):
"Twitter": "\\faTwitter",
"ResearchGate": "\\faResearchgate",
"YouTube": "\\faYoutube",
"Google Scholar": "\\faGraduationCap",
}
for social_network in self.social_networks:
clean_url = social_network.url.replace("https://", "").rstrip("/")
@ -1032,6 +1035,9 @@ class CurriculumVitae(RenderCVBaseModel):
if social_network.network == "StackOverflow":
username = social_network.username.split("/")[1]
connection["placeholder"] = username
if social_network.network == "Google Scholar":
connection["placeholder"] = "Google Scholar"
connections.append(connection)
return connections
@ -1761,25 +1767,14 @@ def generate_json_schema() -> dict[str, Any]:
# already have the required field. Moreover, we would like to warn
# users if they provide null values. They can remove the fields if they
# don't want to provide them.
null_type_dict = {}
null_type_dict["type"] = "null"
null_type_dict = {
"type": "null",
}
for field_name, field in value["properties"].items():
if "anyOf" in field:
if (
len(field["anyOf"]) == 2
and null_type_dict in field["anyOf"]
):
field["oneOf"] = [field["anyOf"][0]]
del field["anyOf"]
if null_type_dict in field["anyOf"]:
field["anyOf"].remove(null_type_dict)
# For sections field of CurriculumVitae:
if "additionalProperties" in field["oneOf"][0]:
field["oneOf"][0]["additionalProperties"]["oneOf"] = (
field["oneOf"][0]["additionalProperties"]["anyOf"]
)
del field["oneOf"][0]["additionalProperties"]["anyOf"]
else:
field["oneOf"] = field["anyOf"]
del field["anyOf"]

File diff suppressed because it is too large Load Diff

View File

@ -283,6 +283,7 @@ def rendercv_filled_curriculum_vitae_data_model(
dm.SocialNetwork(network="GitHub", username="johndoe"),
dm.SocialNetwork(network="Instagram", username="johndoe"),
dm.SocialNetwork(network="Orcid", username="0000-0000-0000-0000"),
dm.SocialNetwork(network="Google Scholar", username="F8IyYrQAAAAJ"),
dm.SocialNetwork(network="Mastodon", username="@johndoe@example"),
dm.SocialNetwork(network="Twitter", username="johndoe"),
dm.SocialNetwork(network="StackOverflow", username="12323/johndoe"),

View File

@ -11,6 +11,7 @@ import typer.testing
import rendercv.cli as cli
import rendercv.data_models as dm
from rendercv import __version__
def run_render_command(input_file_path, working_path, extra_arguments=[]):
@ -509,3 +510,46 @@ def test_create_theme_command_theme_already_exists(tmp_path):
def test_main_file():
subprocess.run([sys.executable, "-m", "rendercv", "--help"], check=True)
def test_get_latest_version_number_from_pypi():
version = cli.get_latest_version_number_from_pypi()
assert isinstance(version, str)
def test_if_welcome_prints_new_version_available(monkeypatch):
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "99999")
import io
import contextlib
with contextlib.redirect_stdout(io.StringIO()) as f:
cli.welcome()
output = f.getvalue()
assert "A new version of RenderCV is available!" in output
def test_rendercv_version_when_there_is_a_new_version(monkeypatch):
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "99999")
result = runner.invoke(cli.app, ["--version"])
assert "A new version of RenderCV is available!" in result.stdout
def test_rendercv_version_when_there_is_not_a_new_version(monkeypatch):
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: __version__)
result = runner.invoke(cli.app, ["--version"])
assert __version__ in result.stdout
def test_warn_if_new_version_is_available(monkeypatch):
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: __version__)
assert not cli.warn_if_new_version_is_available()
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "999")
assert cli.warn_if_new_version_is_available()

View File

@ -450,6 +450,11 @@ def test_invalid_social_networks(network, username):
"@myusername",
"https://youtube.com/@myusername",
),
(
"Google Scholar",
"myusername",
"https://scholar.google.com/citations?user=myusername",
),
],
)
def test_social_network_url(network, username, expected_url):

View File

@ -192,6 +192,8 @@
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}

View File

@ -196,6 +196,10 @@
\kern 5.0 pt%
\AND%
\kern 5.0 pt%
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{scholar.google.com/citations?user=F8IyYrQAAAAJ}}%
\kern 5.0 pt%
\AND%
\kern 5.0 pt%
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
\kern 5.0 pt%
\AND%

View File

@ -51,6 +51,7 @@
\social[github]{johndoe}
\social[instagram]{johndoe}
\social[orcid]{0000-0000-0000-0000}
\social[google scholar]{F8IyYrQAAAAJ}
\social[mastodon]{@johndoe@example}
\social[twitter]{johndoe}
\social[stackoverflow]{12323/johndoe}

View File

@ -168,6 +168,8 @@
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{\color{black}{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}

View File

@ -192,6 +192,8 @@
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}

View File

@ -196,6 +196,10 @@
\kern 5.0 pt%
\AND%
\kern 5.0 pt%
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{scholar.google.com/citations?user=F8IyYrQAAAAJ}}%
\kern 5.0 pt%
\AND%
\kern 5.0 pt%
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
\kern 5.0 pt%
\AND%

View File

@ -51,6 +51,7 @@
\social[github]{johndoe}
\social[instagram]{johndoe}
\social[orcid]{0000-0000-0000-0000}
\social[google scholar]{F8IyYrQAAAAJ}
\social[mastodon]{@johndoe@example}
\social[twitter]{johndoe}
\social[stackoverflow]{12323/johndoe}

View File

@ -168,6 +168,8 @@
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{\color{black}{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
\kern 0.5 cm
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}

View File

@ -8,6 +8,7 @@
- GitHub: [johndoe](https://github.com/johndoe)
- Instagram: [johndoe](https://instagram.com/johndoe)
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
- Mastodon: [@johndoe@example](https://example/@johndoe)
- Twitter: [johndoe](https://twitter.com/johndoe)
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)

View File

@ -8,6 +8,7 @@
- GitHub: [johndoe](https://github.com/johndoe)
- Instagram: [johndoe](https://instagram.com/johndoe)
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
- Mastodon: [@johndoe@example](https://example/@johndoe)
- Twitter: [johndoe](https://twitter.com/johndoe)
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)

View File

@ -8,6 +8,7 @@
- GitHub: [johndoe](https://github.com/johndoe)
- Instagram: [johndoe](https://instagram.com/johndoe)
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
- Mastodon: [@johndoe@example](https://example/@johndoe)
- Twitter: [johndoe](https://twitter.com/johndoe)
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)

View File

@ -8,6 +8,7 @@
- GitHub: [johndoe](https://github.com/johndoe)
- Instagram: [johndoe](https://instagram.com/johndoe)
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
- Mastodon: [@johndoe@example](https://example/@johndoe)
- Twitter: [johndoe](https://twitter.com/johndoe)
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)

Binary file not shown.

View File

@ -8,6 +8,7 @@
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>

View File

@ -8,6 +8,7 @@
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>

View File

@ -8,6 +8,7 @@
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>

View File

@ -8,6 +8,7 @@
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 460 KiB